Skip to content

Route Guards

Protect private routes by checking authentication state before rendering, with redirect preservation.

What Problem This Solves

You need to:

  • Block unauthenticated users from accessing private routes
  • Redirect them to login with the original destination preserved
  • Check token expiry, not just presence
  • Support both token-based and cookie-based auth

Prerequisites

  • @tanstack/react-router
  • Zustand auth store with isAuthenticated and expiresAt

Files

route.tsx (Private Route Layout)

tsx
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
import { Layout } from "@/lib/layout";
import { isTokenExpired, useAuth } from "@/lib/stores/auth";

export const Route = createFileRoute("/_private")({
  beforeLoad: ({ context, location }) => {
    // Optional: disable in dev with env flag
    if (!import.meta.env.VITE_ENABLE_PRIVATE_ROUTE_CHECK) {
      return;
    }

    const isAuthenticated = context.auth.isAuthenticated || !!context.auth.token;

    if (!isAuthenticated || isTokenExpired(context.auth.expiresAt)) {
      useAuth.getState().clearAuth();
      throw redirect({
        to: "/login",
        search: { redirectTo: location.pathname },
      });
    }
  },
  component: RouteComponent,
});

function RouteComponent() {
  return (
    <Layout>
      <Outlet />
    </Layout>
  );
}

__root.tsx (Provide Auth to Context)

tsx
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
import { useAuth, useAuthState } from "@/lib/stores/auth";

interface RouterContext {
  auth: ReturnType<typeof useAuthState>;
}

export const Route = createRootRouteWithContext<RouterContext>()({
  component: RootComponent,
  beforeLoad: () => ({ auth: useAuth.getState() }),
});

function RootComponent() {
  const auth = useAuthState();
  return <Outlet context={{ auth }} />;
}

login.tsx (Consume Redirect)

tsx
import { createFileRoute, useRouter } from "@tanstack/react-router";

export const Route = createFileRoute("/login")({
  component: LoginPage,
});

function LoginPage() {
  const router = useRouter();
  const search = Route.useSearch();

  const handleLoginSuccess = () => {
    const redirectTo = search.redirectTo ?? "/";
    router.navigate({ to: redirectTo });
  };

  return <LoginForm onSuccess={handleLoginSuccess} />;
}

Usage

Place private pages under the _private layout:

src/routes/
  _private/
    route.tsx          <-- guard lives here
    dashboard.tsx
    settings/
      index.tsx
  login.tsx

Any route inside _private/ will automatically run the beforeLoad guard.

Variations

With Access Control (

tsx
beforeLoad: ({ context, location }) => {
  const accessKey = location.pathname; // or route staticData
  const hasAccess = context.auth.accessMenu?.[accessKey];

  if (!hasAccess) {
    throw notFound(); // or redirect to 403 page
  }
};

With Loading State

tsx
beforeLoad: async ({ context }) => {
  if (context.auth.isLoading) {
    await context.auth.waitForInit;
  }
  // ... auth check
};

Minimal Check (

tsx
beforeLoad: ({ context }) => {
  if (!context.auth.token) {
    throw redirect({ to: "/login" });
  }
};