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
isAuthenticatedandexpiresAt
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.tsxAny 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" });
}
};