Skip to content

fix: derive server-backed state instead of mirroring it (Category C, #200) - #369

Open
JohnRDOrazio wants to merge 1 commit into
fix/set-state-in-effect-category-bfrom
fix/set-state-in-effect-category-c
Open

fix: derive server-backed state instead of mirroring it (Category C, #200)#369
JohnRDOrazio wants to merge 1 commit into
fix/set-state-in-effect-category-bfrom
fix/set-state-in-effect-category-c

Conversation

@JohnRDOrazio

Copy link
Copy Markdown
Member

Stacked on #367, which is itself stacked on #365. Base is fix/set-state-in-effect-category-b, so the diff here is just the Category C change. Retarget down the stack as each merges.

Category C of #200server-data-derived state. All three sites.

BranchContext (2 sites)

currentBranch was seeded from the initialBranch prop / sessionStorage pin, then rewritten by two effects: one re-validating it against the branch list every time the query resolved, another applying initialBranch exactly once behind an initialBranchHandled flag.

Both are gone. currentBranch is now a useMemo over the query response and the visitor's explicit selection:

selectedBranch ?? initialBranch ?? storedBranch

Two decisions worth calling out:

Explicit switches are authoritative and not re-validated against the cached list. switchBranch already checks the branch exists, and createBranch selects a branch the server has just created but which the cached list doesn't include until the refetch lands. Only the seeds get validated — which is what the original check was really guarding: a stale pin naming a deleted branch. I got this wrong on the first pass and the existing createBranch test caught it.

initialBranchHandled disappears. It existed to stop the prop being re-applied after the user switched away; the precedence chain says that directly. One nuance: if initialBranch changes before the user has switched, it's now applied, where the old flag ignored anything after the first application. That reads as the more intuitive behaviour for a resume link — flagging it as a deliberate change rather than an accident.

Clearing the stale sessionStorage pin stays in an effect. It writes to sessionStorage and nothing else, so it's synchronisation with an external store, not derived state.

Suggestions page (1 site)

Manual fetch-into-state replaced with a useSuggestionSessions React Query hook shaped like useProject. The hook reports isLoading: false when disabled — a disabled query stays pending forever, which would otherwise spin the page indefinitely for signed-out visitors.

This is the site #200 flagged as overlapping #89; #89's migration (PR #105) is merged, so this was the leftover.

How I verified the BranchContext change

Coverage for the branch-resolution logic was thin, so I wrote eight characterisation tests against the previous implementation first and confirmed they passed before touching anything:

  • initialBranch kept when the server confirms it
  • sessionStorage pin kept when it still exists
  • deleted pin → preferred_branch, and the stale pin cleared
  • deleted pin, no preference → current_branch
  • preferred_branch ignored when it isn't in the list
  • unauthenticated visitors pinned to the default, ignoring any seed
  • a user switch surviving a later refetch
  • "main" before the server responds

All eight still pass unchanged after the refactor, alongside the 17 pre-existing tests.

Verification against the #367 base

  • react-hooks/set-state-in-effect: 9 → 6 warnings.
  • npm run type-check clean, npm run lint 0 errors, npm run build succeeds.
  • npx vitest --run — 161 files / 2772 tests pass.

Where this leaves #200

With this merged, the only remaining warnings are the four Category D sites in #362, the one #154 removes, and the re-filed app/projects/[id]/editor/page.tsx:311 — which still needs your call on latch-vs-live derivation before it can be done. Once those land the rule can graduate to "error".

…200)

All three Category C sites. Each held a copy of server state in local
state and refilled it from an effect once the data landed.

BranchContext (2 sites)
  `currentBranch` was seeded from the initialBranch prop / sessionStorage
  pin, then rewritten by one effect that re-validated it against the
  branch list and a second that applied `initialBranch` once behind an
  `initialBranchHandled` flag. Both are gone; `currentBranch` is now a
  useMemo over the query response and the visitor's explicit selection.

  An explicit switch is authoritative and deliberately not re-validated
  against the cached list — `switchBranch` already checks the branch
  exists, and `createBranch` selects one the server has just created but
  which the cached list does not include until the refetch lands. Only
  the *seeds* are validated, which is what the original check was really
  protecting against: a stale pin naming a deleted branch.

  Clearing that stale pin stays in an effect. It writes to sessionStorage
  and nothing else, so it is synchronisation, not derived state.

  `initialBranchHandled` disappears. It existed to stop the prop being
  re-applied after the user switched away; precedence
  (`selectedBranch ?? initialBranch ?? storedBranch`) expresses that
  directly. One behavioural nuance: if `initialBranch` *changes* before
  the user has switched, it is now applied, where the old flag would have
  ignored anything after the first application. That is the more
  intuitive reading of a resume link.

suggestions page (1 site)
  Manual fetch-into-state replaced with a `useSuggestionSessions` React
  Query hook, matching useProject's shape. The hook reports isLoading as
  false when disabled, since a disabled query stays pending forever and
  would otherwise spin for signed-out visitors.

`react-hooks/set-state-in-effect`: 9 warnings → 6.

Tests: eight characterisation tests were written against the *previous*
BranchContext implementation first — seed precedence, deleted-pin
fallback to preferred_branch then current_branch, the unauthenticated
lock, and survival of a switch across a refetch — and all still pass
unchanged after the refactor. They caught one real difference along the
way: the first derivation re-validated explicit selections and broke
create-then-switch, which is why selections are now exempt. Five new
tests cover useSuggestionSessions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 88c33211-3f60-44f0-9ef3-051b48bff573

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.29630% with 1 line in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
lib/context/BranchContext.tsx 94.11% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant