Domphy

Router Context

Router context is a typed object available to every route's beforeLoad, loader, and component. It's the correct way to share dependencies (auth state, query clients, analytics) with routes without globals.

Defining context type

import { createRootRouteWithContext, createRouter } from "@domphy/router"
import { QueryClient } from "@domphy/query"

interface RouterContext {
  queryClient: QueryClient
  auth: {
    isAuthenticated: () => boolean
    user: () => User | null
  }
  featureFlags: {
    newDashboard: boolean
    betaSearch: boolean
  }
}

const rootRoute = createRootRouteWithContext<RouterContext>()({
  component: () => RootLayout,
})

Providing context when creating the router

const queryClient = new QueryClient()

const router = createRouter({
  routeTree: rootRoute.addChildren([...]),
  context: {
    queryClient,
    auth: {
      isAuthenticated: () => !!localStorage.getItem("token"),
      user: () => JSON.parse(localStorage.getItem("user") ?? "null"),
    },
    featureFlags: {
      newDashboard: import.meta.env.VITE_NEW_DASHBOARD === "true",
      betaSearch: false,
    },
  },
})

Accessing context in beforeLoad

const dashboardRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: "/dashboard",
  beforeLoad: ({ context }) => {
    if (!context.auth.isAuthenticated()) {
      throw redirect({ to: "/login" })
    }
  },
})

Accessing context in loaders

const postsRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: "/posts",
  loader: async ({ context }) => {
    // Use queryClient from context — no global import needed
    return context.queryClient.ensureQueryData({
      queryKey: ["posts"],
      queryFn: fetchPosts,
    })
  },
  component: () => PostsList,
})

Accessing context in components

Access router context in a component via router.options.context:

const ProfilePage = {
  div: (l) => {
    const user = router.options.context.auth.user()
    return { h1: `Welcome, ${user?.name ?? "Guest"}` }
  },
}

Or pass it through loaderData:

const userRoute = createRoute({
  loader: ({ context }) => ({ user: context.auth.user() }),
  component: () => {
    const match = router.state.matches.find(m => m.routeId === userRoute.id)
    const user = match?.loaderData?.user
    return { h1: `Welcome, ${user?.name ?? "Guest"}` }
  },
})

Dynamic context (reactive values)

If context values change after the router is created (e.g. auth state updates), pass an accessor function instead of a static value:

import { toState } from "@domphy/core"

const authState = toState<{ user: User | null; token: string | null }>({
  user: null,
  token: localStorage.getItem("token"),
})

const router = createRouter({
  routeTree,
  context: {
    // These functions read from reactive state — always current
    auth: {
      isAuthenticated: () => !!authState.get().token,
      user: () => authState.get().user,
    },
    queryClient,
  },
})

// When auth changes, context.auth.user() automatically returns the new value
async function login(credentials: Credentials) {
  const { user, token } = await loginApi(credentials)
  authState.set({ user, token })
  localStorage.setItem("token", token)
  router.navigate({ to: "/dashboard" })
}

Per-route context modification

A route can add to the context for its children using beforeLoad's return value:

const orgRoute = createRoute({
  getParentRoute: () => authRoute,
  path: "/org/$orgId",
  beforeLoad: async ({ params, context }) => {
    const org = await context.queryClient.ensureQueryData({
      queryKey: ["org", params.orgId],
      queryFn: () => fetchOrg(params.orgId),
    })
    // Return additional context — available to all child routes
    return { org }
  },
})

const settingsRoute = createRoute({
  getParentRoute: () => orgRoute,
  path: "/settings",
  loader: ({ context }) => {
    // context.org is available here — provided by orgRoute.beforeLoad
    return fetchOrgSettings(context.org.id)
  },
})

TypeScript: typed context

With createRootRouteWithContext<RouterContext>(), TypeScript fully types all context accesses:

// beforeLoad context is RouterContext — full type safety
beforeLoad: ({ context }) => {
  context.auth.isAuthenticated()   // ✓ () => boolean
  context.queryClient.prefetchQuery   // ✓ QueryClient method
  context.featureFlags.newDashboard   // ✓ boolean
  context.auth.unknownMethod()   // ✗ TypeScript error
}