Skip to content

refactor(router): one nav-tree model - #29610

Draft
chrisnojima wants to merge 1 commit into
masterfrom
nojima/HOTPOT-arch-06-nav-tree
Draft

refactor(router): one nav-tree model#29610
chrisnojima wants to merge 1 commit into
masterfrom
nojima/HOTPOT-arch-06-nav-tree

Conversation

@chrisnojima

@chrisnojima chrisnojima commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Problem

The navigation tree shape — root.routes[0] === 'loggedIn' → tab navigator → per-tab stack, with root.routes[1..] holding modals and phone-pushed screens — was re-derived by hand in eight functions across two modules.

constants/router.tsx:872 and router-v2/linking.tsx:53 built literally the same phone nav-state object — and had already drifted: linking set index: 0 on the loggedIn child state, navToThread omitted it.

router-v2/linking-state.test.ts admitted the duplication in a comment:

spelled out here rather than reusing makeChatConversationState, so a bug shared by the builder and the path parser cannot pass unnoticed

— a test working around a missing module rather than testing one.

Change

New constants/nav-tree.tsx, dependency-free (one runtime import, a zero-import leaf; everything else import type):

  • Readers: visiblePath / visibleScreen / modalStack / activeStack / currentTab / isLoggedIn / tabNavigatorState
  • Builders: tabState / modalState / pushedAboveTabs
  • Invariants stated once: the setModalRouteNames registry and tabRoots

Pure functions over a plain NavState. No navigationRef, no dispatch, no chat imports.

Also types stores/router.tsx's navState?: unknown as NavState, killing the casts in fs/common/daemon.tsx, chat/inbox-and-conversation-shared.tsx and constants/init/shared.tsx.

Drift resolved

index: 0 is now always spelled out. The two routers disagree on a missing index:

  • SwitchRouter.getRehydratedStatestate.routes[state.index ?? 0] — defaults to 0
  • StackRouter.getRehydratedStatestate.index ?? routes.length - 1 — defaults to last

For a one-route tab state both agreed, so the historical drift was benign — but the same omission reads differently at other depths, so the explicit form wins.

Second drift found while unifying: navToThread emitted {name: 'chatRoot', params: {}} where the fs deep link emitted {name: 'fsRoot'}. The builder now omits params; since chatRoot declares initialParams: {}, StackRouter computes the same {} either way.

Validation

lint:all clean — 0 bailed out, 0 whole-props deps, tsc clean both projects.
jest --runInBand230 suites / 2251 tests (baseline 230 / 2236).

constants/tests/router-visible.test.tsnav-tree.test.ts, 5 → 20 tests; the builders were untested before.

The navigation tree shape - routes[0] of the root stack is the 'loggedIn' tab
navigator, routes[1..] holds modals and phone-pushed screens - was re-derived by
hand in eight functions across constants/router and router-v2/linking, and had
already drifted: navToThread's phone branch omitted the `index: 0` that
makeChatConversationState set on the loggedIn child state.

constants/nav-tree owns the shape now. Readers (visiblePath, visibleScreen,
modalStack, activeStack, currentTab, tabNavigatorState, isLoggedIn) are pure
functions of a NavState; builders (tabState, modalState, pushedAboveTabs)
return a PartialNavState. The modal-name registry and tabRoots live there too,
so modal-ness-by-name and tab roots are stated once. No navigationRef, no
dispatch, no chat.

Two literal differences were resolved in favour of one shape:
- `index: 0` on the loggedIn child state is now always spelled out. It is what
  react-navigation computes anyway (a tab router rehydrates a missing index to
  0), but a stack router rehydrates a missing index to the LAST route, so
  leaving it off means the same omission reads differently at different depths.
- the tab-root route under a pushed screen now carries no `params`, matching
  what the fs deep link already built. chatRoot declares `initialParams: {}`, so
  StackRouter rehydrates `{...{}, ...undefined}` to the same `{}` the old
  `params: {}` produced.

The store's `navState?: unknown` is typed by NavState, which removes the casts
at its three read sites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rccpV5nLxxC5opF5xzrz7
@chrisnojima
chrisnojima force-pushed the nojima/HOTPOT-arch-06-nav-tree branch from 77849d6 to 9249e64 Compare September 11, 2026 01:47
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