React Router Patterns
SDK: @clerk/react-router v3+. Requires React Router v7.9+.
What Do You Need?
Task Reference
Auth in loaders and actions references/loaders-actions.md
Protected routes and redirects references/protected-routes.md
SSR user data and session references/ssr-auth.md
Mental Model
React Router v7 uses a middleware + loader pipeline. Clerk plugs into both layers:
-
Middleware (clerkMiddleware() ) — runs on every request, attaches auth to context
-
rootAuthLoader — required in root.tsx to pass Clerk state to the client
-
getAuth(args) — called inside any loader/action to get the current user
Request → clerkMiddleware() → rootAuthLoader → page loader → component ↓ ↓ ↓ attaches auth injects state getAuth(args) to context to response reads context
Minimal Setup
- root.tsx
import { rootAuthLoader } from '@clerk/react-router/server' import { ClerkApp } from '@clerk/react-router' import type { Route } from './+types/root'
export async function loader(args: Route.LoaderArgs) { return rootAuthLoader(args) }
export default ClerkApp(function App() { return <Outlet /> })
- Middleware (root route or entry.server.ts)
import { clerkMiddleware } from '@clerk/react-router/server' export const middleware = [clerkMiddleware()]
Required: rootAuthLoader must be called in root.tsx 's loader. Without it, getAuth throws in nested loaders.
Auth in Loaders
import { getAuth } from '@clerk/react-router/server' import type { Route } from './+types/dashboard'
export async function loader(args: Route.LoaderArgs) { const { userId } = await getAuth(args) if (!userId) throw redirect('/sign-in')
const data = await fetchUserData(userId) return { data } }
Auth in Actions
import { getAuth } from '@clerk/react-router/server'
export async function action(args: Route.ActionArgs) { const { userId, orgId } = await getAuth(args) if (!userId) throw new Response('Unauthorized', { status: 401 })
const formData = await args.request.formData() await saveData(userId, orgId, formData) return redirect('/dashboard') }
Client Components
import { useAuth, useUser } from '@clerk/react-router'
export function Profile() { const { userId, isSignedIn } = useAuth() const { user } = useUser() if (!isSignedIn) return null return <p>{user?.firstName}</p> }
Org Switching
import { OrganizationSwitcher } from '@clerk/react-router'
export function Nav() { return <OrganizationSwitcher afterSelectOrganizationUrl="/dashboard" /> }
export async function loader(args: Route.LoaderArgs) { const { userId, orgId } = await getAuth(args) if (!userId) throw redirect('/sign-in') if (!orgId) throw redirect('/select-org')
return { data: await fetchOrgData(orgId) } }
Common Pitfalls
Symptom Cause Fix
clerkMiddleware() not detected
Missing middleware Export middleware = [clerkMiddleware()] from root route
getAuth returns empty userId rootAuthLoader not called Call rootAuthLoader(args) in root.tsx loader
Infinite redirect loop Redirect target is also protected Exclude /sign-in from protection check
redirect not working in action Using Response instead of throw redirect()
Use throw redirect('/path') from react-router
Import Map
What Import From
getAuth
@clerk/react-router/server
rootAuthLoader
@clerk/react-router/server
clerkMiddleware
@clerk/react-router/server
ClerkApp
@clerk/react-router
useAuth , useUser
@clerk/react-router
OrganizationSwitcher
@clerk/react-router
See Also
-
clerk-setup
-
Initial Clerk install
-
clerk-custom-ui
-
Custom flows & appearance
-
clerk-orgs
-
B2B organizations
Docs
React Router SDK