Step 1 of phase 2 of the App.tsx decomposition, landed first and alone
because it is the seam the remaining clusters are extracted onto.
App.tsx kept six "latest value" refs so long-lived callbacks could read a
current value without listing it as a dependency and being re-created on
every change: `activeServerNameRef`, `activeServerIdRef`, `serversRef`,
`inspectorClientRef`, `pendingStepUpRef` and `pendingReauthRef`, plus the
three passive effects that synced them. `useSessionRef` replaces all nine
with one hook returning one stable ref onto a `SessionSnapshot`.
This is not a store: nothing renders off it, nothing subscribes to it, and
mutating it never schedules a render. It is not the prop-to-state sync
AGENTS.md forbids either — that rule is about `setState` in an effect, and
a ref write produces no render at all. It is exactly the pattern the file
already used, written once instead of six times.
What it buys the later steps is a *stable* dependency: a hook taking
`sessionRef` does not drag the other clusters into its argument list, so
`useOAuthRecovery`, `useConnectionLifecycle` and `useServerCommands` can
be extracted one PR at a time.
Behavior is unchanged:
- The snapshot is seeded from the first render's values, matching what
each `useRef(value)` seeded.
- `activeServerName` keeps its one non-uniform rule — it holds the last
server that *was* active and is never cleared, because a transport
crash clears `activeServerId` before the failure toast is raised. It is
derived from `servers`/`activeServerId` rather than taking the
`activeServer` memo, which is the same value by definition and is what
lets the hook be called ahead of that memo.
- The two pending-* syncs already ran ahead of the only effect that reads
them, so merging them changes no effect's view. The other four synced
below every effect that reads them, so hoisting that sync changes none
either.
- The sync effect is deliberately un-gated rather than carrying a
dependency list: re-assigning unchanged values costs nothing and
removes a place for a newly-added field to be forgotten.
`sessionRef` is declared ahead of its first reader because a hook return
is not provably stable to `react-hooks/exhaustive-deps` — consumers must
list it, and a later declaration would put every one of them in the
temporal dead zone. Listing a ref that never changes identity re-runs
nothing.
The inline `pendingStepUp` state shape is named `PendingStepUp` and moved
beside `StepUpSource` in `utils/stepUp.ts`, so the snapshot can name it
without importing from the component that owns the state.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MZdpUwhHf943FoprcLVzEm
Signed-off-by: cliffhall <cliff@futurescale.com>