22 chapters · 80 min
Skills
Chapter 17 of 22
React Router v7/v8 patterns with Clerk — rootAuthLoader, getAuth in loaders, clerkMiddleware, protected routes, SSR user data, org switching.
2 minutes · 429 words · 16 sections
SDK: @clerk/react-router v3.5+. Supports React Router v7.9+ and v8.
| 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 |
Check the installed react-router major version before scaffolding — the config differs:
| v7.9+ | v8+ | |
|---|---|---|
| Middleware API | Opt-in: set future: { v8_middleware: true } in react-router.config.ts | Always on — do NOT set the flag (v8 removed it) |
ssr.noExternal workaround (below) | Not needed | Required |
React Router v8 ships development/production conditional exports. In react-router dev,
Vite externalizes @clerk/react-router for SSR, so Node resolves the production build of
react-router while the app code gets the development build — two module instances, two
Router contexts. Every request then fails during SSR with:
Error: useNavigate() may be used only in the context of a <Router> component.npm ls react-router shows a single copy — that does NOT rule this out. The
duplication is per export condition, not per installed copy. Do not chase duplicate
installs; add the workaround (upstream issue:
https://github.com/remix-run/react-router/issues/15232 (opens in a new tab)):
import { reactRouter } from '@react-router/dev/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [reactRouter()],
ssr: {
noExternal: ['@clerk/react-router'],
},
})import { Outlet } from 'react-router'
import { rootAuthLoader, clerkMiddleware } from '@clerk/react-router/server'
import { ClerkProvider } from '@clerk/react-router'
import type { Route } from './+types/root'
export const middleware: Route.MiddlewareFunction[] = [clerkMiddleware()]
There is no ClerkApp HOC in @clerk/react-router (that was the @clerk/remix API).
Render <ClerkProvider loaderData={loaderData}> inside the default export and pass it
the root route’s loaderData.
import type { Config } from '@react-router/dev/config'
export default {
future: {
v8_middleware: true,
},
} satisfies ConfigOn v8, omit the future block entirely — the flag no longer exists.
Required:
rootAuthLoadermust be called inroot.tsx‘s loader. Without it,getAuththrows in nested loaders.
React Router v7/v8 uses a middleware + loader pipeline. Clerk plugs into both layers:
clerkMiddleware()) — runs on every request, attaches auth to contextrootAuthLoader — required in root.tsx to pass Clerk state to the clientgetAuth(args) — called inside any loader/action to get the current userRequest → clerkMiddleware() → rootAuthLoader → page loader → component
↓ ↓ ↓
attaches auth injects state getAuth(args)
to context to response reads contextimport { 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(
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:
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
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'
| Symptom | Cause | Fix |
|---|---|---|
useNavigate() may be used only in the context of a <Router> thrown from ClerkProvider during SSR in dev (v8) | Vite dev SSR externalizes @clerk/react-router, which then loads react-router’s production build while the app uses the development build — two Router contexts. A single copy in npm ls does not rule this out. | Add ssr: { noExternal: ['@clerk/react-router'] } to vite.config.ts. Do NOT downgrade to v7 |
Build error: ClerkApp is not exported | ClerkApp does not exist in @clerk/react-router | Use <ClerkProvider loaderData={loaderData}> in root.tsx’s default export |
clerkMiddleware() not detected | Missing middleware (or on v7, missing v8_middleware future flag) | Export middleware = [clerkMiddleware()] from root route; on v7 also set future: { v8_middleware: true } |
| Unknown future flag error/warning (v8) | v8_middleware flag left in react-router.config.ts after upgrading | Remove the future.v8_middleware entry — middleware is always on in v8 |
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 |
| What | Import From |
|---|---|
getAuth | @clerk/react-router/server |
rootAuthLoader | @clerk/react-router/server |
clerkMiddleware | @clerk/react-router/server |
ClerkProvider | @clerk/react-router |
useAuth, useUser | @clerk/react-router |
OrganizationSwitcher | @clerk/react-router |
clerk-setup - Initial Clerk installclerk-custom-ui - Custom flows & appearanceclerk-orgs - B2B organizationsInstall this repository
npx skills add clerk/skills/plugin marketplace add clerk/skillsSkills install per repository, not per chapter — the CLI has no documented per-skill form, so we do not print one.
React Router v7/v8 patterns with Clerk — rootAuthLoader, getAuth in loaders, clerkMiddleware, protected routes, SSR user data, org switching. Triggers on: react-router auth, rootAuthLoader, getAuth loader, react-router protected route, loader authentication, SSR auth react-router, useNavigate may be used only in the context of a Router.
The verbatim description from this skill’s front matter — the string an agent matches on to decide whether to load it.
WebFetchskills/frameworks/clerk-react-router-patterns/SKILL.mdmain, last pushed 6 August 2026.SKILL.md, not by matching a directory convention. 5 distinct layouts observed: .agents/skills/*/SKILL.md, skills/core/*/SKILL.md, skills/features/*/SKILL.md, skills/frameworks/*/SKILL.md, skills/mobile/*/SKILL.md.h1 and no skipped levels:.claude-plugin/marketplace.json by Clerk, declaring 4 plugins. It is read for editorial metadata only — never as the skill index, which is always the repository tree./clerk/skills.md, and each chapter at its own .md URL.11 files · 13 KB
Everything this skill ships beside its prose. All of it is set here, as subchapters of chapter 17.
Documentation the agent loads on demand, rather than up front.
Templates, schemas and fixtures the skill draws on.
Everything else published alongside the skill.