clerk-react-router-patterns
Installation
SKILL.md
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 inroot.tsxto pass Clerk state to the clientgetAuth(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
1. 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 />
})
2. Middleware (root route or entry.server.ts)
import { clerkMiddleware } from '@clerk/react-router/server'
export const middleware = [clerkMiddleware()]
Required:
rootAuthLoadermust be called inroot.tsx's loader. Without it,getAuththrows 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 installclerk-custom-ui- Custom flows & appearanceclerk-orgs- B2B organizations
Docs
Weekly Installs
195
Repository
clerk/skillsGitHub Stars
34
First Seen
7 days ago
Security Audits
Installed on
codex184
opencode182
gemini-cli181
deepagents181
github-copilot181
amp181