name: TanStack Router Stability SPFx description: Stable singleton pattern for TanStack Router v1 preventing route-tree recreation and UI freezes on AppContext changes in SPFx version: 2.0 category: router triggers: tanstack router, router stability, useRef router, router recreation, navigation freeze, project picker freeze, router.update, updateContext, adapter hooks, useAppNavigate, useAppLocation, useAppParams updated: 2026-02-21
TanStack Router Stability SPFx Skill
Activation Implementing, modifying, or debugging TanStack Router provider, route guards, navigation components, ProjectPicker, adapter hooks, or any scenario where navigation freezes after dashboard load or project switch.
Protocol
- Router singleton: Create router instance exactly once via
useRefinrouter.tsxwith static values only (queryClient,dataService). NEVER recreate on dynamic value changes. - Dynamic injection: Use
React.useEffect->router.update({ context: { currentUser, selectedProject, isFeatureEnabled, scope } })for all dynamic values.RouterProvider context={}syncs the React tree. - Adapter hook stability:
useAppNavigatereturns a ref-stable callback (identity never changes).useAppLocationreturns auseMemo-stabilised{ pathname, search }.useAppParamsreturnsuseMemo-stabilised params withJSON.stringifydep. - ProjectPicker ordering:
handleSelectMUST close popover (setIsOpen(false)) and clear query BEFORE firing context mutation. Context mutation wrapped inReact.startTransition(() => onSelect(project)). - App.tsx stability: Suspense fallback lifted to module-level constant. Router props wrapped in
useMemoto avoid unnecessaryrouter.update()calls. - Post-change verification: Run
tsc --noEmit+jest --no-coverage+ update CLAUDE.md S4 and S16.
6 Critical Flows Guaranteed Stable
- Project switch — popover closes visually before context mutation (via
startTransition); router updates in-place viarouter.update() - Route navigation —
useTransitionNavigatereturns a stable callback; all navigations wrapped instartTransition - Location tracking —
useAppLocationreturns memoised{ pathname, search }; NavigationSidebar active-state checks only re-run when path actually changes - Route params —
useAppParamsreturns memoised params; downstream components (LeadDetail, GoNoGo) don't re-render on unrelated router state changes - Permission/feature gate re-evaluation — flows through
router.update()context merge; guards see latestisFeatureEnabledwithout router recreation - Breadcrumb/telemetry — consume stable
useTransitionNavigate/useAppLocation; no unnecessary re-renders
Manual Test Steps
- Hub dashboard -> open ProjectPicker -> select project -> verify popover closes instantly, no freeze, sidebar updates
- Navigate to any module (e.g.,
/operations/monthly-review) -> verify no blank screen or remount - Navigate to lead drill-down (
/lead/:id) -> verify params load correctly - Press browser back button -> verify smooth return, no route tree destruction
- Switch role via RoleSwitcher -> verify guards re-evaluate without navigation freeze
- Observe breadcrumb trail during all above — should update without flicker
Reference
CLAUDE.mdS4 (Router Stability Rule), S16 (Active Pitfalls)PERFORMANCE_OPTIMIZATION_GUIDE.mdS4 (React 18 & Context Rules)SECURITY_PERMISSIONS_GUIDE.mdS1 (TanStack Router guard enforcement)react-context-and-concurrentskill (context splitting complement)