4.5 KiB
4.5 KiB
auth-route-protection: Protect Routes with beforeLoad
Priority: HIGH
Explanation
Use beforeLoad in route definitions to check authentication before the route loads. This prevents unauthorized access, redirects to login, and can extend context with user data for child routes.
Bad Example
// Checking auth in component - too late, data may have loaded
function DashboardPage() {
const user = useAuth()
useEffect(() => {
if (!user) {
navigate({ to: '/login' }) // Redirect after render
}
}, [user])
if (!user) return null // Flash of content possible
return <Dashboard user={user} />
}
// No protection on route
export const Route = createFileRoute('/dashboard')({
loader: async () => {
// Fetches sensitive data even for unauthenticated users
return await fetchDashboardData()
},
component: DashboardPage,
})
Good Example: Route-Level Protection
// routes/_authenticated.tsx - Layout route for protected area
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'
import { getSessionData } from '@/lib/session.server'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ location }) => {
const session = await getSessionData()
if (!session) {
throw redirect({
to: '/login',
search: {
redirect: location.href,
},
})
}
// Extend context with user for all child routes
return {
user: session,
}
},
component: AuthenticatedLayout,
})
function AuthenticatedLayout() {
return (
<div>
<AuthenticatedNav />
<main>
<Outlet /> {/* Child routes render here */}
</main>
</div>
)
}
// routes/_authenticated/dashboard.tsx
// This route is automatically protected by parent
export const Route = createFileRoute('/_authenticated/dashboard')({
loader: async ({ context }) => {
// context.user is guaranteed to exist
return await fetchDashboardData(context.user.id)
},
component: DashboardPage,
})
function DashboardPage() {
const data = Route.useLoaderData()
const { user } = Route.useRouteContext()
return <Dashboard data={data} user={user} />
}
Good Example: Role-Based Access
// routes/_admin.tsx
export const Route = createFileRoute('/_admin')({
beforeLoad: async ({ context }) => {
// context.user comes from parent _authenticated route
if (context.user.role !== 'admin') {
throw redirect({ to: '/unauthorized' })
}
},
component: AdminLayout,
})
// File structure:
// routes/
// _authenticated.tsx # Requires login
// _authenticated/
// dashboard.tsx # /dashboard - any authenticated user
// settings.tsx # /settings - any authenticated user
// _admin.tsx # Admin layout
// _admin/
// users.tsx # /users - admin only
// analytics.tsx # /analytics - admin only
Good Example: Preserving Redirect URL
// routes/login.tsx
import { z } from 'zod'
export const Route = createFileRoute('/login')({
validateSearch: z.object({
redirect: z.string().optional(),
}),
component: LoginPage,
})
function LoginPage() {
const { redirect } = Route.useSearch()
const loginMutation = useMutation({
mutationFn: login,
onSuccess: () => {
// Redirect to original destination or default
navigate({ to: redirect ?? '/dashboard' })
},
})
return <LoginForm onSubmit={loginMutation.mutate} />
}
// In protected routes
beforeLoad: async ({ location }) => {
if (!session) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
}
Good Example: Conditional Content Based on Auth
// Public route with different content for logged-in users
export const Route = createFileRoute('/')({
beforeLoad: async () => {
const session = await getSessionData()
return { user: session?.user ?? null }
},
component: HomePage,
})
function HomePage() {
const { user } = Route.useRouteContext()
return (
<div>
<Hero />
{user ? (
<PersonalizedContent user={user} />
) : (
<SignUpCTA />
)}
</div>
)
}
Context
beforeLoadruns before route loading begins- Throwing
redirect()prevents route from loading - Context from
beforeLoadflows to loader and component - Child routes inherit parent's
beforeLoadprotection - Use pathless layout routes (
_authenticated.tsx) for grouped protection - Store redirect URL in search params for post-login navigation