Release 0.2.1: PRView Rules-of-Hooks fix (React #310 blank page)

PRView.jsx declared its threadsByKind useMemo after the early-return
guard for the loading state. First mount: pr is null, early return
fires, 14 hooks called. Second render: pr populated, execution
reaches the useMemo, 15 hooks — React #310, page goes blank.

Move the useMemo above the early returns and null-safe the
pr?.threads access so the hook count stays stable across renders.

Pre-existing bug in the post-Contribute-rewrite PR view; surfaced
in production by 0.2.0's graduation merge race fix enabling the
workflow that opens this code path. Patch per §20.2 — no operator
action required beyond rebuilding the frontend and restarting.
This commit is contained in:
Ben Stull
2026-05-26 08:50:48 -07:00
parent 29c96ea300
commit 0e1805b8ce
5 changed files with 39 additions and 15 deletions
+13 -11
View File
@@ -143,6 +143,19 @@ export default function PRView({ viewer }) {
}
}, [slug, prNumber, reviewText, reviewDraft, refresh])
// §10.6: split messages by thread kind for the visual distinction
// §10.4 requires. Within each kind, sort by id for chronological
// order. Declared above the early returns below so the hook count
// stays stable between the loading render (pr == null) and the
// loaded render (Rules of Hooks — React #310 otherwise).
const threadsByKind = useMemo(() => {
const out = { chat: [], review: [], flag: [] }
for (const t of pr?.threads || []) {
out[t.thread_kind]?.push(t)
}
return out
}, [pr?.threads])
// ── render ─────────────────────────────────────────────────────
if (error) return <article className="entry-view"><p>Error: {error}</p></article>
if (!pr) return <article className="entry-view">Loading PR</article>
@@ -154,17 +167,6 @@ export default function PRView({ viewer }) {
const supersededBy = pr.superseded_by_pr_number
const supersedes = pr.supersedes_pr_number
// §10.6: split messages by thread kind for the visual distinction
// §10.4 requires. Within each kind, sort by id for chronological
// order.
const threadsByKind = useMemo(() => {
const out = { chat: [], review: [], flag: [] }
for (const t of pr.threads || []) {
out[t.thread_kind]?.push(t)
}
return out
}, [pr.threads])
const seenSha = pr.seen?.last_seen_commit_sha
const seenMsgId = pr.seen?.last_seen_message_id || 0