Resolve command routes with route-level loading instead of mount effects
- Dominant language
- TypeScript
- Stars
- 481
- Forks
- 108
- Avg merge
- 6d 8h
- Merged PRs (30d)
- 5
Description
## Description
Three routes in the app are commands rather than places. They name something to do, then leave:
- `#/connect` resolves a connection link, activates or prompts, then redirects (`routes/Connect`)
- the `*` catch-all redirects to the graph view (`components/Redirect.tsx`, whose entire body is a mount effect that navigates)
- `/data-explorer` redirects to the first node type when none is given (`routes/DataExplorer`)
Declarative routing (``) has no vocabulary for that, so each one smuggles a one-shot action into a component lifecycle. #1828 worked through three variants of this for the connect route and every one left a residue:
- deriving the decision each render churns object identity, so the effect acting on it needs `useEffectEvent`
- resolving inside the effect and calling `setState` trips the lint rule against `setState` in an effect, and shows the create form a render late
- resolving in a `useState` initializer works, but resolution happens during render, and it mints a connection id there (harmless, since a StrictMode double-invoke discards the extra, but it is a wart)
None is wrong. They are the same compromise wearing different clothes. A routing layer with loaders removes the compromise instead of relocating it: resolve before the component renders, redirect without ever mounting, and let a component render only when there is something to show.
There is a second, larger benefit. One-shot resolution in the connect route is only correct today because `AppStatusLoader` happens to gate the route behind a loading state until default connections arrive. That dependency is incidental and undocumented in code. Route-level loading would make it explicit, because the loader can await the connections it depends on.
## Preferred Solution
Move to a routing layer with route-level loading, then convert the three command routes to it. Two candidates worth comparing before committing.
**React Router 8 data mode.** Already the installed dependency, so this is a reconfiguration rather than a replacement: `createHashRouter` plus `RouterProvider` in place of `` plus ``. Existing router hooks keep working, so most components are untouched.
**TanStack Router.** Since the routing layer would be changing substantially either way, it is worth evaluating rather than assumed away. Points that matter for this codebase: typed routes and typed search parameters, first-class search-parameter validation with Zod adapters (connection links already validate their search parameters with Zod, so that logic could move into the route definition), loaders, and its own approach to code splitting. Against it: a full dependency replacement, unfamiliarity, and every router hook call site in the app would change.
The evaluation should produce a recommendation with a migration estimate for each, not just a preference.
## What makes this feasible
Worth recording, because it is what makes loaders practical here at all:
- `getAppStore()` is `getDefaultStore()`, jotai's module-level singleton, so a loader can read app state without hooks.
- `storageAtoms.ts` preloads IndexedDB with a top-level `await` before the atoms are created, so `store.get(configurationAtom)` is a plain synchronous read of real data from anywhere. No hydration dance in the loader.
- `toast` is a module-level API, so a loader can notify before redirecting.
## The real design work
Default connections are currently fetched by a TanStack Query call and written into the store by an effect in `AppStatusLoader`, which renders a loading state while the store is empty. In data mode, loaders run **before** the component tree renders, so a connect loader would run before `AppStatusLoader` ever mounts and would miss default connections entirely. That breaks the exact case pinned by the test "does not prompt to create when a loading default connection matches the connect URL".
Fixing it properly means moving default-connection loading out of a component effect and into route loading, so any route that depends on connections can await them. That is the right architecture and it is also a change to the app's boot sequence, which is why this is its own issue rather than a detail of the router swap.
## Expected outcome
- A recommendation between React Router data mode and TanStack Router, with migration estimates
- Default-connection loading expressed as route loading rather than a component effect
- The three command routes converted, deleting `components/Redirect.tsx` outright
- An ADR recording the choice, superseding the routing decisions in `docs/adr/20260612-connection-links.md`
## Related Issues
- Originated from #1828
- Related to #1788
> [!IMPORTANT]
> Internal only — this issue is maintained by the core team and is not accepting external contributions.
Contributor guide
Research direction
Compare React Router 8 data mode with TanStack Router, then read AppStatusLoader and storageAtoms.ts to trace default-connection loading before the component tree renders. Inspect routes/Connect, routes/DataExplorer, components/Redirect.tsx, and the test named “does not prompt to create when a loading default connection matches the connect URL.” Done means a documented recommendation with migration estimates, route loading for defaults and the three command routes, removal of Redirect.tsx, and an ADR superseding docs/adr/20260612-connection-links.md.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 20/100