Skip to content

Routing With TanStack Router

The webapp uses TanStack Router v1 with file-based routes, generated route types, route groups, and TanStack Query integration.

Router Setup

src/main.tsx creates the router from src/routeTree.gen.ts and passes the shared Query Client in router context:

tsx
const router = createRouter({
  routeTree,
  context: {
    ...TanstackQuery.getContext(),
  },
  defaultPreload: 'intent',
  scrollRestoration: true,
  defaultStructuralSharing: true,
  defaultPreloadStaleTime: 0,
  defaultPendingMs: 150,
  defaultPendingMinMs: 300,
})

The Vite router plugin generates src/routeTree.gen.ts. Never edit that file by hand.

Representative Route Tree

text
src/routes/
├── __root.tsx
├── index.tsx
├── (public)/
│   ├── route.tsx
│   ├── sign-in.tsx
│   ├── sign-up.tsx
│   ├── forgot-password.tsx
│   └── reset-password.tsx
└── (authenticated)/
    ├── route.tsx
    ├── account/route.tsx
    ├── (onboarding)/
    │   ├── route.tsx
    │   └── onboarding/route.tsx
    └── (workspace)/
        ├── route.tsx
        ├── dashboard/route.tsx
        └── events/new/route.tsx

Parenthesized route groups organize guards and layouts without adding their names to URLs.

Route Responsibilities

Root Route

src/routes/__root.tsx defines router context and global route behavior. It renders HeadContent, the active Outlet, the themed toaster, and router development tools. It also supplies the root error and not-found components.

Public Boundary

src/routes/(public)/route.tsx handles the public route family. Signed-in users are redirected away from credential routes. Individual routes own their metadata, search validation, and error UI.

The sign-in route validates its redirect search value with Zod. It accepts only a safe internal path and falls back to /dashboard, then restores that destination after a successful sign-in. Reset-password similarly reads its token from validated route search state.

Authenticated Boundary

src/routes/(authenticated)/route.tsx uses beforeLoad to require a session. The guard reads the session through the Query Client and redirects signed-out users to /sign-in, preserving the requested location in the redirect search parameter.

Keep authentication checks at this route boundary rather than repeating them in page components.

Onboarding And Workspace Boundaries

The onboarding boundary redirects users who already have an organization. The workspace boundary requires organization data before it renders workspace pages. Its loader:

  1. Fetches the user's organizations with createOrganizationsQueryOptions().
  2. Redirects to /onboarding when none exist.
  3. Fetches the active organization.
  4. Selects the first organization when no active organization exists, invalidates that exact cache entry, and fetches it again.
  5. Returns the organizations and active organization to the route.
tsx
async function loadWorkspaceOrganizations(queryClient: QueryClient) {
  const organizations = await queryClient.fetchQuery(
    createOrganizationsQueryOptions(auth.authClient),
  )

  const [firstOrganization] = organizations
  if (!firstOrganization) throw redirect({ to: '/onboarding' })

  const activeOrganizationQueryOptions = createActiveOrganizationQueryOptions(auth.authClient)
  let activeOrganization = await queryClient.fetchQuery(activeOrganizationQueryOptions)

  if (!activeOrganization) {
    const result = await auth.authClient.organization.setActive({
      organizationId: firstOrganization.id,
    })
    if (result.error) throw result.error

    await queryClient.invalidateQueries({
      queryKey: activeOrganizationQueryOptions.queryKey,
      exact: true,
      refetchType: 'none',
    })
    activeOrganization = await queryClient.fetchQuery(activeOrganizationQueryOptions)
  }

  return { organizations, activeOrganization }
}

The same route defines its pending and error components. On success, it wraps AppLayout in WorkspaceProvider so descendant components can read the cached workspace state.

Adding Or Changing Routes

  1. Place the route under the public, authenticated, onboarding, or workspace boundary that matches its data requirements.
  2. Define it with createFileRoute() and keep search validation, guards, loaders, pending UI, and route errors in the route module.
  3. Use the configured queryClient, auth, and api surfaces instead of constructing clients in a route.
  4. Let the router plugin regenerate routeTree.gen.ts.
  5. Use TanStack Router's typed Link, useNavigate, and route APIs for navigation and route state.

Use State Management to decide whether a value belongs in route search, the Query cache, a form, or a component. For framework behavior, see the TanStack Router documentation.

Built with ❤️ by the Jubiloop team