Skip to content

Refactor SSR router state to use AsyncLocalStorage #646

@natew

Description

@natew

Summary

Currently, the router uses module-level state that persists across SSR requests. We work around this by calling globalThis['__vxrnresetState']?.() before each render, but this doesn't handle concurrent requests safely.

This issue tracks refactoring to use AsyncLocalStorage for proper per-request state isolation.

Current Implementation

Problem: 14+ module-level variables in router.ts persist across requests:

  • initialized, routeNode, rootComponent
  • Navigation state: initialState, rootState, nextState, routeInfo
  • navigationRef, navigationRefSubscription
  • 3 subscriber Set collections
  • linkingConfig

Current workaround in oneServe.ts:184:

// Reset router state for each SSR request to ensure correct routing
// TODO: Consider using AsyncLocalStorage to isolate router state per request
// instead of using global reset, for better concurrency handling
globalThis['__vxrnresetState']?.()

Proposed Solution

Use AsyncLocalStorage (already used successfully in one-server-only.tsx for per-request headers).

1. Create routerAsyncLocalStore.ts

import { AsyncLocalStorage } from 'node:async_hooks'

interface RouterContextState {
  initialized: boolean
  routeNode: RouteNode | null
  rootState: NavigationState | undefined
  nextState: ResultState | undefined
  routeInfo: UrlObject | undefined
  navigationRef: NavigationContainerRef<any> | null
  // ... other state
}

export const ROUTER_CONTEXT_STORE = new AsyncLocalStorage<RouterContextState>()

export function getRouterContext() {
  return ROUTER_CONTEXT_STORE.getStore()
}

export function runWithRouterContext<T>(fn: () => T): T {
  return ROUTER_CONTEXT_STORE.run(createInitialContext(), fn)
}

2. Update router.ts

Replace module-level variable access with context getters.

3. Wrap SSR rendering in oneServe.ts

const response = await runWithRouterContext(async () => {
  const rendered = await (await getRender())({ ... })
  return new Response(rendered, { headers, status })
})

Benefits

  • True request isolation (no state leakage between concurrent requests)
  • Concurrency safe for parallel SSR requests
  • Automatic cleanup per request
  • Removes manual reset workaround
  • Follows existing pattern in codebase (one-server-only.tsx)

Considerations

  • Keep cacheable data (preloadingLoader, cssInjectFunctions) at module level for performance
  • Need fallback for Vercel Lambda (doesn't support getStore() - but existing code has fallback patterns)
  • Only applies to SSR path, not client-side

Files to Modify

  • packages/one/src/router/router.ts - Move state to AsyncLocalStorage
  • packages/one/src/router/useInitializeOneRouter.ts - Update initialization
  • packages/one/src/router/linkingConfig.ts - Move linkingConfig to context
  • packages/one/src/server/oneServe.ts - Wrap rendering in context
  • packages/one/src/cli/buildPage.ts - Wrap SSG rendering in context

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions