Implement authentication flows with protected routes, redirects, and context-based access control.
Setup auth context
Create an authentication context for your router:
export interface AuthContext {
isAuthenticated: boolean
user: User | null
login: (username: string, password: string) => Promise<void>
logout: () => Promise<void>
}
export function createAuthContext(): AuthContext {
let user: User | null = null
return {
get isAuthenticated() {
return user !== null
},
get user() {
return user
},
async login(username, password) {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
})
user = await response.json()
},
async logout() {
await fetch('/api/logout', { method: 'POST' })
user = null
},
}
}
Router with auth context
Provide auth context to all routes:
import { createRouter, RouterProvider } from '@tanstack/react-router'
import { createAuthContext } from './auth'
import { routeTree } from './routeTree.gen'
const auth = createAuthContext()
const router = createRouter({
routeTree,
context: {
auth,
},
})
// Register for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
function App() {
return <RouterProvider router={router} />
}
Protected routes
Guard routes with authentication checks:
src/routes/_authenticated.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: {
// Redirect back after login
redirect: location.href,
},
})
}
},
})
src/routes/
_authenticated.tsx -> Auth layout (checks auth)
_authenticated/
dashboard.tsx -> /dashboard (protected)
profile.tsx -> /profile (protected)
settings.tsx -> /settings (protected)
Login page
Implement login with redirect:
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { z } from 'zod'
const loginSearchSchema = z.object({
redirect: z.string().optional(),
})
export const Route = createFileRoute('/login')({
validateSearch: loginSearchSchema,
component: LoginPage,
})
function LoginPage() {
const navigate = useNavigate()
const { redirect } = Route.useSearch()
const { auth } = Route.useRouteContext()
const [error, setError] = React.useState<string>()
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const username = formData.get('username') as string
const password = formData.get('password') as string
try {
await auth.login(username, password)
// Redirect to original destination or dashboard
navigate({ to: redirect || '/dashboard' })
} catch (err) {
setError('Invalid credentials')
}
}
return (
<form onSubmit={handleSubmit}>
<input name="username" type="text" required />
<input name="password" type="password" required />
{error && <p className="error">{error}</p>}
<button type="submit">Login</button>
</form>
)
}
Role-based access
Protect routes based on user roles:
src/routes/_authenticated/_admin.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/_admin')({
beforeLoad: async ({ context }) => {
if (context.auth.user?.role !== 'admin') {
throw redirect({
to: '/unauthorized',
})
}
},
})
src/routes/
_authenticated/
_admin/
users.tsx -> /users (admin only)
settings.tsx -> /settings (admin only)
Conditional navigation
Show links based on permissions:
import { Link } from '@tanstack/react-router'
function Navigation() {
const { auth } = Route.useRouteContext()
return (
<nav>
<Link to="/">Home</Link>
{auth.isAuthenticated ? (
<>
<Link to="/dashboard">Dashboard</Link>
<Link to="/profile">Profile</Link>
{auth.user?.role === 'admin' && (
<Link to="/admin">Admin</Link>
)}
<button onClick={auth.logout}>Logout</button>
</>
) : (
<Link to="/login">Login</Link>
)}
</nav>
)
}
Session management
Check and refresh authentication:
import { createRootRouteWithContext } from '@tanstack/react-router'
import type { AuthContext } from '@/auth'
interface RouterContext {
auth: AuthContext
}
export const Route = createRootRouteWithContext<RouterContext>()({
beforeLoad: async ({ context }) => {
// Check session on every navigation
try {
const response = await fetch('/api/session')
if (!response.ok) {
context.auth.user = null
}
} catch (err) {
context.auth.user = null
}
},
})
Token-based auth
Store and use JWT tokens:
export function createAuthContext() {
const getToken = () => localStorage.getItem('auth_token')
const setToken = (token: string) => localStorage.setItem('auth_token', token)
const clearToken = () => localStorage.removeItem('auth_token')
return {
get isAuthenticated() {
return !!getToken()
},
async login(username: string, password: string) {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
})
const { token } = await response.json()
setToken(token)
},
async logout() {
clearToken()
},
async fetch(url: string, options?: RequestInit) {
const token = getToken()
return fetch(url, {
...options,
headers: {
...options?.headers,
Authorization: `Bearer ${token}`,
},
})
},
}
}
OAuth/Social login
Implement OAuth flows:
src/routes/auth/callback.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
import { z } from 'zod'
const callbackSearchSchema = z.object({
code: z.string(),
state: z.string().optional(),
})
export const Route = createFileRoute('/auth/callback')({
validateSearch: callbackSearchSchema,
loader: async ({ search, context }) => {
// Exchange code for token
const response = await fetch('/api/oauth/token', {
method: 'POST',
body: JSON.stringify({ code: search.code }),
})
const { token } = await response.json()
localStorage.setItem('auth_token', token)
// Redirect to app
throw redirect({ to: '/dashboard' })
},
})
Logout handling
Clear session and redirect:
function LogoutButton() {
const navigate = useNavigate()
const { auth } = Route.useRouteContext()
const handleLogout = async () => {
await auth.logout()
navigate({ to: '/login' })
}
return <button onClick={handleLogout}>Logout</button>
}
Persisting auth state
Restore auth on page reload:
const auth = createAuthContext()
// Restore session on app load
const restoreSession = async () => {
try {
const response = await fetch('/api/session')
if (response.ok) {
const user = await response.json()
auth.user = user
}
} catch (err) {
console.error('Failed to restore session', err)
}
}
restoreSession().then(() => {
const router = createRouter({ routeTree, context: { auth } })
// ... render app
})
Best practices
Use pathless layout routes
Wrap protected routes in _authenticated layout for clean URLs.
Use beforeLoad instead of loader for auth checks to avoid data fetching for unauthorized users.
Store minimal data in context
Only store essential auth state - fetch user details when needed.
Implement token refresh logic and redirect to login on expiration.
Always use HTTPS to protect authentication tokens and credentials.
Security considerations
Never store sensitive data like passwords in localStorage. Use secure, httpOnly cookies for tokens when possible.
Always validate user permissions on the server. Client-side checks are for UX only.
Implement CSRF protection for state-changing operations.
Next steps
Data loading
Load user data in protected routes
beforeLoad hook
Learn more about beforeLoad