Route Guards
Route guards allow you to protect routes by running logic before navigation occurs. Common use cases include authentication, authorization, data validation, and redirects.
Overview
TanStack Router provides the beforeLoad hook that runs before a route is loaded. Use it to:
Check authentication status
Verify user permissions
Validate required context
Redirect users to appropriate pages
Perform side effects before rendering
Authentication Guards
Protect routes that require authentication:
File-Based Routing
import { createFileRoute , redirect } from '@tanstack/react-router'
import { Outlet } from '@tanstack/react-router'
export const Route = createFileRoute ( '/_auth' )({
beforeLoad : async ({ context , location }) => {
// Check if user is authenticated
if ( ! context . auth . isAuthenticated ) {
// Redirect to login with return URL
throw redirect ({
to: '/login' ,
search: {
redirect: location . href ,
},
})
}
},
component: AuthLayout ,
})
function AuthLayout () {
return (
< div >
< h1 > Authenticated Area </ h1 >
< Outlet />
</ div >
)
}
All child routes inherit the guard:
src/routes/_auth/dashboard.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute ( '/_auth/dashboard' )({
component : () => < div > Dashboard - Auth required! </ div > ,
})
Code-Based Routing
import { createRoute , redirect } from '@tanstack/react-router'
const authLayoutRoute = createRoute ({
getParentRoute : () => rootRoute ,
id: '_auth' ,
beforeLoad : ({ context , location }) => {
if ( ! context . auth . isAuthenticated ) {
throw redirect ({
to: '/login' ,
search: { redirect: location . href },
})
}
},
component: AuthLayout ,
})
const dashboardRoute = createRoute ({
getParentRoute : () => authLayoutRoute ,
path: '/dashboard' ,
component: Dashboard ,
})
Setting Up Auth Context
Provide authentication state to all routes via router context:
import { createRouter , RouterProvider } from '@tanstack/react-router'
import { useAuth } from './auth'
function App () {
const auth = useAuth ()
const router = createRouter ({
routeTree ,
context: {
auth ,
},
})
return < RouterProvider router = { router } />
}
import { useState , useEffect } from 'react'
export interface AuthContext {
isAuthenticated : boolean
user : User | null
login : ( email : string , password : string ) => Promise < void >
logout : () => Promise < void >
}
export function useAuth () : AuthContext {
const [ user , setUser ] = useState < User | null >( null )
useEffect (() => {
// Check session on mount
checkSession (). then ( setUser )
}, [])
return {
isAuthenticated: !! user ,
user ,
login : async ( email , password ) => {
const user = await loginUser ( email , password )
setUser ( user )
},
logout : async () => {
await logoutUser ()
setUser ( null )
},
}
}
Type the router context:
declare module '@tanstack/react-router' {
interface Register {
router : typeof router
}
interface RouterContext {
auth : AuthContext
}
}
Authorization Guards
Check user permissions and roles:
src/routes/_auth/admin.tsx
import { createFileRoute , redirect } from '@tanstack/react-router'
export const Route = createFileRoute ( '/_auth/admin' )({
beforeLoad : ({ context }) => {
const { auth } = context
// Check authentication
if ( ! auth . isAuthenticated ) {
throw redirect ({ to: '/login' })
}
// Check authorization
if ( ! auth . user ?. roles . includes ( 'admin' )) {
throw redirect ({
to: '/' ,
// Optionally show error message
search: { error: 'Insufficient permissions' },
})
}
},
component: AdminPanel ,
})
Multiple Guard Conditions
Combine multiple checks:
export const Route = createFileRoute ( '/_auth/workspace/$workspaceId' )({
beforeLoad : async ({ context , params }) => {
const { auth } = context
// 1. Check authentication
if ( ! auth . isAuthenticated ) {
throw redirect ({ to: '/login' })
}
// 2. Verify workspace access
const hasAccess = await checkWorkspaceAccess (
auth . user . id ,
params . workspaceId ,
)
if ( ! hasAccess ) {
throw redirect ({
to: '/workspaces' ,
search: { error: 'Workspace not found or access denied' },
})
}
// 3. Load workspace data
const workspace = await fetchWorkspace ( params . workspaceId )
// Return data to pass to route context
return { workspace }
},
component: WorkspaceView ,
})
Login Route with Redirect
Capture and redirect after successful login:
import { createFileRoute , redirect , useNavigate } from '@tanstack/react-router'
import { useAuth } from '../auth'
export const Route = createFileRoute ( '/login' )({
validateSearch : ( search : Record < string , unknown >) => {
return {
redirect: ( search . redirect as string ) || '/' ,
}
},
beforeLoad : ({ context , search }) => {
// If already logged in, redirect
if ( context . auth . isAuthenticated ) {
throw redirect ({ to: search . redirect })
}
},
component: LoginPage ,
})
function LoginPage () {
const auth = useAuth ()
const navigate = useNavigate ()
const { redirect } = Route . useSearch ()
const handleLogin = async ( email : string , password : string ) => {
await auth . login ( email , password )
// Redirect to original destination
navigate ({ to: redirect })
}
return (
< form onSubmit = { ( e ) => {
e . preventDefault ()
const formData = new FormData ( e . currentTarget )
handleLogin (
formData . get ( 'email' ) as string ,
formData . get ( 'password' ) as string ,
)
} } >
< input name = "email" type = "email" required />
< input name = "password" type = "password" required />
< button type = "submit" > Login </ button >
</ form >
)
}
Preloading with Guards
Guards also run during preloading:
import { Link } from '@tanstack/react-router'
// This will trigger beforeLoad when hovering
< Link to = "/dashboard" preload = "intent" >
Dashboard
</ Link >
Guards run during preload, but redirects are only executed during actual navigation.
Conditional Rendering vs Guards
Use guards for navigation control, not just UI:
// ❌ Wrong: User can still access via direct URL
function Dashboard () {
const { auth } = useRouteContext ()
if ( ! auth . isAuthenticated ) {
return < div > Please log in </ div >
}
return < div > Dashboard </ div >
}
// ✅ Correct: Navigation is blocked
export const Route = createFileRoute ( '/dashboard' )({
beforeLoad : ({ context }) => {
if ( ! context . auth . isAuthenticated ) {
throw redirect ({ to: '/login' })
}
},
component: Dashboard ,
})
Async Guards
Perform async operations in guards:
export const Route = createFileRoute ( '/_auth/organization/$orgId' )({
beforeLoad : async ({ context , params }) => {
// Async permission check
const hasPermission = await checkOrgPermission (
context . auth . user . id ,
params . orgId ,
)
if ( ! hasPermission ) {
throw redirect ({ to: '/organizations' })
}
// Optionally load and return data
const organization = await fetchOrganization ( params . orgId )
return { organization }
},
})
Error Handling in Guards
Handle errors gracefully:
export const Route = createFileRoute ( '/protected' )({
beforeLoad : async ({ context }) => {
try {
// Verify session with backend
await verifySession ( context . auth . sessionToken )
} catch ( error ) {
// Session expired or invalid
context . auth . logout ()
throw redirect ({
to: '/login' ,
search: { error: 'Session expired' },
})
}
},
})
Composing Guards
Reuse guard logic:
import { redirect } from '@tanstack/react-router'
export function requireAuth ({ context , location } : any ) {
if ( ! context . auth . isAuthenticated ) {
throw redirect ({
to: '/login' ,
search: { redirect: location . href },
})
}
}
export function requireRole ( role : string ) {
return ({ context } : any ) => {
requireAuth ({ context })
if ( ! context . auth . user ?. roles . includes ( role )) {
throw redirect ({ to: '/' })
}
}
}
Use in routes:
import { requireAuth , requireRole } from '../utils/guards'
export const Route = createFileRoute ( '/_auth/admin' )({
beforeLoad: requireRole ( 'admin' ),
component: AdminPanel ,
})
Best Practices
Use Redirects Always throw redirect() instead of returning JSX from guards
Validate Async Perform server-side validation, don’t trust client state alone
Context Over Props Use router context for auth state, not prop drilling
Guard Hierarchies Place guards on layout routes to protect all children
Security : Route guards are client-side only. Always validate permissions on your backend API as well.
Complete Example
Here’s a full authentication setup:
import { createRootRoute } from '@tanstack/react-router'
import { AuthProvider } from '../auth'
export const Route = createRootRoute ({
component : () => (
< AuthProvider >
< Outlet />
</ AuthProvider >
),
})
export const Route = createFileRoute ( '/_auth' )({
beforeLoad : ({ context , location }) => {
if ( ! context . auth . isAuthenticated ) {
throw redirect ({
to: '/login' ,
search: { redirect: location . href },
})
}
},
component: AuthLayout ,
})
Next Steps
Error Boundaries Handle errors in protected routes
SSR Server-side rendering with authentication