Skip to content
Twilight React Playground
ثيم رائدaren

createRouter

functionAdvancedserverbrowserlive demo

Builds the theme's TanStack router with the engine's defaults: a data cache, SSR hydration, a loading skeleton, and the error and 404 pages.

import { createRouter, CreateRouterOptions, RouterInitialContext } from '@salla.sa/twilight-theme-engine/tanstack';

In plain words

A router decides which page to show for an address, loads that page's data and moves between pages without reloading. TanStack Start asks your theme for one by calling getRouter() in app/router.tsx.

createRouter(routeTree) builds it with what the engine needs already switched on: a data cache (a TanStack Query QueryClient), a skeleton while a page loads, the engine's 404 and error pages, and the code that carries the server's data into the browser. routeTree is the file the build generates from your routes, app/routeTree.gen.ts.

On the server a new router is built for every request. In the browser you keep the first one, so the data cache survives page changes.

Signature

function createRouter(routeTree: any, options?: CreateRouterOptions): Router

interface CreateRouterOptions {
  defaultPendingComponent?: (() => ReactNode) | false;  // default <PageSkeleton />; false: none
  defaultPendingMs?: number;                            // default 100
  defaultPendingMinMs?: number;                         // default 200
  defaultStaleTime?: number;                            // default Infinity
  history?: RouterHistory;
}

interface RouterInitialContext {   // the context every route receives
  queryClient: QueryClient;
  setLocation: (location: Partial<TwilightLocation>) => void;
}

Try it live

The router and QueryClient that createRouter() built for this very page, read back from the running app.Try this: look at rewrite: on localhost and the preview host it is on, because there the store is the first segment of every URL.
Storefront canvas · ar · RTL
Runs in the browser…
What a theme writes
import { createRouter } from '@salla.sa/twilight-theme-engine/tanstack';
import { routeTree } from './routeTree.gen';

// One router in the browser (keeps the QueryClient cache), a fresh one per server request.
let clientRouter: ReturnType<typeof createRouter> | null = null;

export function getRouter() {
  if (typeof window !== 'undefined' && clientRouter) return clientRouter;

  const router = createRouter(routeTree, {
    defaultPendingMs: 100,
    defaultPendingMinMs: 200,
  });

  if (typeof window !== 'undefined') clientRouter = router;
  return router;
}

declare module '@tanstack/react-router' {
  interface Register {
    router: ReturnType<typeof getRouter>;
  }
}

Example

app/router.tsx
import { createRouter } from '@salla.sa/twilight-theme-engine/tanstack';
import {
  registerHomeComponents,
  DefaultHomeComponents,
} from '@salla.sa/twilight-theme-engine/routes/home';
import { routeTree } from './routeTree.gen';

registerHomeComponents(DefaultHomeComponents);

// One router in the browser, so the QueryClient cache survives navigations.
// The server builds a fresh one per request through getRouter().
let clientRouter: ReturnType<typeof createRouter> | null = null;

export function getRouter() {
  if (typeof window !== 'undefined' && clientRouter) return clientRouter;

  const router = createRouter(routeTree, {
    defaultPendingMs: 100,
    defaultPendingMinMs: 200,
  });

  if (typeof window !== 'undefined') clientRouter = router;
  return router;
}

declare module '@tanstack/react-router' {
  interface Register {
    router: ReturnType<typeof getRouter>;
  }
}

How it behaves

  • Fixed, not options: defaultPreload: 'intent' (hovering or focusing a link loads the next page's data), scroll restoration with instant behaviour, trailingSlash: 'never', the lazy NotFoundPage as the 404 page, and an error component that renders the engine ErrorPage as 404, 401, 400 or 500 depending on the error.

  • Each call creates a new QueryClient (queries: staleTime 60 s, gcTime 5 min, retry 1, no refetch on window focus; mutations: no retry) and writes it into the twilight context, where loaders read it as getTwilightContext().queryClient.

  • It installs the TanStack head adapter for the whole app (setHeadAdapter), so resolveHead() from /utils/head returns TanStack's meta and links arrays from then on.

  • On localhost and preview.salla.design it adds a rewrite: the store's username is removed from each URL before matching and added to every href the router builds. Your routes stay /{-$locale}/cart.

  • For SSR it wires TanStack Query's integration (the cache travels with the HTML) and, in the browser, copies settings, locale, auth token, location, page and route id from the root match into the twilight context before head functions run again.

  • In the browser it keeps the context in step: when a page's loaders finish it stores their page object and passes it to Salla.config.set('page', …); when a navigation resolves it updates location, route id and settings, and dispatches the SDK event route::changed.

  • RouterInitialContext is the root route's context type: beforeLoad and loaders receive context.queryClient. The engine's type declarations already register it with TanStack's RouterContext.

Gotchas

  • Build it once in the browser. Every call creates a new QueryClient and makes it the context's client, so a getRouter() that returns a new router on each call throws away the cache the page was hydrated with. Keep the clientRouter variable of the example.

  • defaultStaleTime is Infinity: going back to a page you already visited shows the data its loader returned then, without running the loader again, until TanStack drops the cached match (its default gcTime, 5 minutes) or you call router.invalidate(). Pass a number, such as defaultStaleTime: 30_000, when pages must refetch on return.

  • On the server it throws [Twilight] No request context… unless twilightMiddleware() ran first, because it writes the new QueryClient into the request context.

Related

Source and docs