← ClerkCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Clerk
Snapshot Sep 30, 2026 · 23:09 UTC · version 0.1.0
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"name": "clerk-react-router-patterns",
"description": "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.",
"included_files": [
{
"relative_path": "evals/evals.json",
"size_in_bytes": 4749
},
{
"relative_path": "references/loaders-actions.md",
"size_in_bytes": 1742
},
{
"relative_path": "references/protected-routes.md",
"size_in_bytes": 1712
},
{
"relative_path": "references/ssr-auth.md",
"size_in_bytes": 1470
},
{
"relative_path": "templates/react-router-basic-auth/app/app.css",
"size_in_bytes": 0
},
{
"relative_path": "templates/react-router-basic-auth/app/root.tsx",
"size_in_bytes": 2504
},
{
"relative_path": "templates/react-router-basic-auth/app/routes.ts",
"size_in_bytes": 132
},
{
"relative_path": "templates/react-router-basic-auth/app/routes/home.tsx",
"size_in_bytes": 57
},
{
"relative_path": "templates/react-router-basic-auth/package.json",
"size_in_bytes": 427
},
{
"relative_path": "templates/react-router-basic-auth/react-router.config.ts",
"size_in_bytes": 105
},
{
"relative_path": "templates/react-router-basic-auth/vite.config.ts",
"size_in_bytes": 499
}
],
"skill_md_contents": "---\nname: clerk-react-router-patterns\ndescription: 'React Router v7/v8 patterns with Clerk — rootAuthLoader, getAuth in loaders,\n clerkMiddleware, protected routes, SSR user data, org switching. Triggers on: react-router\n auth, rootAuthLoader, getAuth loader, react-router protected route, loader authentication,\n SSR auth react-router, useNavigate may be used only in the context of a Router.'\nlicense: MIT\nallowed-tools: WebFetch\nmetadata:\n author: clerk\n version: 1.1.0\n---\n\n# React Router Patterns\n\nSDK: `@clerk/react-router` v3.5+. Supports React Router v7.9+ and v8.\n\n## What Do You Need?\n\n| Task | Reference |\n|------|-----------|\n| Auth in loaders and actions | references/loaders-actions.md |\n| Protected routes and redirects | references/protected-routes.md |\n| SSR user data and session | references/ssr-auth.md |\n\n## React Router v7 vs v8\n\nCheck the installed `react-router` major version before scaffolding — the config differs:\n\n| | v7.9+ | v8+ |\n|--|--|--|\n| Middleware API | Opt-in: set `future: { v8_middleware: true }` in `react-router.config.ts` | Always on — do NOT set the flag (v8 removed it) |\n| `ssr.noExternal` workaround (below) | Not needed | **Required** |\n\n## Minimal Setup\n\n### 1. vite.config.ts (v8 only — REQUIRED)\n\nReact Router v8 ships development/production conditional exports. In `react-router dev`,\nVite externalizes `@clerk/react-router` for SSR, so Node resolves the production build of\nreact-router while the app code gets the development build — two module instances, two\nRouter contexts. Every request then fails during SSR with:\n\n```\nError: useNavigate() may be used only in the context of a <Router> component.\n```\n\n**`npm ls react-router` shows a single copy — that does NOT rule this out.** The\nduplication is per export condition, not per installed copy. Do not chase duplicate\ninstalls; add the workaround (upstream issue:\nhttps://github.com/remix-run/react-router/issues/15232):\n\n```ts\nimport { reactRouter } from '@react-router/dev/vite'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n plugins: [reactRouter()],\n ssr: {\n noExternal: ['@clerk/react-router'],\n },\n})\n```\n\n### 2. root.tsx\n\n```tsx\nimport { Outlet } from 'react-router'\nimport { rootAuthLoader, clerkMiddleware } from '@clerk/react-router/server'\nimport { ClerkProvider } from '@clerk/react-router'\nimport type { Route } from './+types/root'\n\nexport const middleware: Route.MiddlewareFunction[] = [clerkMiddleware()]\n\nexport async function loader(args: Route.LoaderArgs) {\n return rootAuthLoader(args)\n}\n\nexport default function App({ loaderData }: Route.ComponentProps) {\n return (\n <ClerkProvider loaderData={loaderData}>\n <Outlet />\n </ClerkProvider>\n )\n}\n```\n\nThere is no `ClerkApp` HOC in `@clerk/react-router` (that was the `@clerk/remix` API).\nRender `<ClerkProvider loaderData={loaderData}>` inside the default export and pass it\nthe root route's `loaderData`.\n\n### 3. react-router.config.ts (v7 only)\n\n```ts\nimport type { Config } from '@react-router/dev/config'\n\nexport default {\n future: {\n v8_middleware: true,\n },\n} satisfies Config\n```\n\nOn v8, omit the `future` block entirely — the flag no longer exists.\n\n> **Required**: `rootAuthLoader` must be called in `root.tsx`'s loader. Without it, `getAuth` throws in nested loaders.\n\n## Mental Model\n\nReact Router v7/v8 uses a middleware + loader pipeline. Clerk plugs into both layers:\n\n- **Middleware** (`clerkMiddleware()`) — runs on every request, attaches auth to context\n- **`rootAuthLoader`** — required in `root.tsx` to pass Clerk state to the client\n- **`getAuth(args)`** — called inside any loader/action to get the current user\n\n```\nRequest → clerkMiddleware() → rootAuthLoader → page loader → component\n ↓ ↓ ↓\n attaches auth injects state getAuth(args)\n to context to response reads context\n```\n\n## Auth in Loaders\n\n```tsx\nimport { getAuth } from '@clerk/react-router/server'\nimport type { Route } from './+types/dashboard'\n\nexport async function loader(args: Route.LoaderArgs) {\n const { userId } = await getAuth(args)\n if (!userId) throw redirect('/sign-in')\n\n const data = await fetchUserData(userId)\n return { data }\n}\n```\n\n## Auth in Actions\n\n```tsx\nimport { getAuth } from '@clerk/react-router/server'\n\nexport async function action(args: Route.ActionArgs) {\n const { userId, orgId } = await getAuth(args)\n if (!userId) throw new Response('Unauthorized', { status: 401 })\n\n const formData = await args.request.formData()\n await saveData(userId, orgId, formData)\n return redirect('/dashboard')\n}\n```\n\n## Client Components\n\n```tsx\nimport { useAuth, useUser } from '@clerk/react-router'\n\nexport function Profile() {\n const { userId, isSignedIn } = useAuth()\n const { user } = useUser()\n if (!isSignedIn) return null\n return <p>{user?.firstName}</p>\n}\n```\n\n## Org Switching\n\n```tsx\nimport { OrganizationSwitcher } from '@clerk/react-router'\n\nexport function Nav() {\n return <OrganizationSwitcher afterSelectOrganizationUrl=\"/dashboard\" />\n}\n```\n\n```tsx\nexport async function loader(args: Route.LoaderArgs) {\n const { userId, orgId } = await getAuth(args)\n if (!userId) throw redirect('/sign-in')\n if (!orgId) throw redirect('/select-org')\n\n return { data: await fetchOrgData(orgId) }\n}\n```\n\n## Common Pitfalls\n\n| Symptom | Cause | Fix |\n|---------|-------|-----|\n| `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 |\n| Build error: `ClerkApp` is not exported | `ClerkApp` does not exist in `@clerk/react-router` | Use `<ClerkProvider loaderData={loaderData}>` in root.tsx's default export |\n| `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 }` |\n| 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 |\n| `getAuth` returns empty userId | `rootAuthLoader` not called | Call `rootAuthLoader(args)` in `root.tsx` loader |\n| Infinite redirect loop | Redirect target is also protected | Exclude `/sign-in` from protection check |\n| `redirect` not working in action | Using `Response` instead of `throw redirect()` | Use `throw redirect('/path')` from `react-router` |\n\n## Import Map\n\n| What | Import From |\n|------|-------------|\n| `getAuth` | `@clerk/react-router/server` |\n| `rootAuthLoader` | `@clerk/react-router/server` |\n| `clerkMiddleware` | `@clerk/react-router/server` |\n| `ClerkProvider` | `@clerk/react-router` |\n| `useAuth`, `useUser` | `@clerk/react-router` |\n| `OrganizationSwitcher` | `@clerk/react-router` |\n\n## See Also\n\n- `clerk-setup` - Initial Clerk install\n- `clerk-custom-ui` - Custom flows & appearance\n- `clerk-orgs` - B2B organizations\n\n## Docs\n\n[React Router SDK](https://clerk.com/docs/react-router/getting-started/quickstart)\n"
}SHA-256: 65a395965ff166c8be282a55a3d2d2beb1d791e0d9f4da6db8ec1392e62a1107