From 0e1805b8ceab0646a2df78193549d90175256834 Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Tue, 26 May 2026 08:50:48 -0700 Subject: [PATCH] Release 0.2.1: PRView Rules-of-Hooks fix (React #310 blank page) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 22 ++++++++++++++++++++++ VERSION | 2 +- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- frontend/src/components/PRView.jsx | 24 +++++++++++++----------- 5 files changed, 39 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ec7302..1e40fe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,28 @@ skip versions are the composition of each intervening adjacent release's steps in order — no A-to-B path is pre-computed beyond that. +## 0.2.1 — 2026-05-26 + +**Patch — no operator action required.** Rebuild the frontend and +restart per the routine deploy steps; no `.env` changes, no schema +changes, no behavior changes a deployment would notice in steady +state. + +### Fixed + +- **PR view blank page (React #310).** `frontend/src/components/PRView.jsx` + declared its `threadsByKind` `useMemo` *after* the early-return + guard for the loading state (`if (!pr) return …`). On first mount + `pr` was null so the early return fired and 14 hooks were called; + on the second render `pr` was populated and execution reached the + `useMemo`, calling 15 hooks — violating the Rules of Hooks and + unmounting the page subtree (blank screen). The `useMemo` is now + declared above the early returns with optional-chaining on `pr`, + keeping the hook count stable between the loading and loaded + 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. + ## 0.2.0 — 2026-05-26 **Breaking config change.** The frontend now requires diff --git a/VERSION b/VERSION index 0ea3a94..0c62199 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.0 +0.2.1 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3f03771..7a70921 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "rfc-app-frontend", - "version": "0.1.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rfc-app-frontend", - "version": "0.1.0", + "version": "0.2.1", "dependencies": { "@codemirror/commands": "^6.10.3", "@codemirror/lang-markdown": "^6.5.0", diff --git a/frontend/package.json b/frontend/package.json index 565c486..fa730fe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "rfc-app-frontend", "private": true, - "version": "0.2.0", + "version": "0.2.1", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/PRView.jsx b/frontend/src/components/PRView.jsx index 15363d9..3c11457 100644 --- a/frontend/src/components/PRView.jsx +++ b/frontend/src/components/PRView.jsx @@ -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

Error: {error}

if (!pr) return
Loading PR…
@@ -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