All skills
clerk avatar

/clerk-react-router-patterns

@23e7108 official
by clerkclerk/skills83 stars
5

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.

Use this Skill: https://skilld.dev/gh/clerk/skills/clerk-react-router-patterns

This session only. Nothing lands on disk.

SKILL.md

β‰ˆ92 tokens always: the name and description. β‰ˆ1.7k when used: this file. β‰ˆ2.5k more on demand in 5 files.

React Router Patterns

SDK: @clerk/react-router v3.5+. Supports React Router v7.9+ and v8.

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

React Router v7 vs v8

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

Minimal Setup

1. vite.config.ts (v8 only β€” 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):

import { reactRouter } from '@react-router/dev/vite'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [reactRouter()],
  ssr: {
    noExternal: ['@clerk/react-router'],
  },
})

2. root.tsx

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()]

export async function loader(args: Route.LoaderArgs) {
  return rootAuthLoader(args)
}

export default function App({ loaderData }: Route.ComponentProps) {
  return (
    <ClerkProvider loaderData={loaderData}>
      <Outlet />
    </ClerkProvider>
  )
}

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.

3. react-router.config.ts (v7 only)

import type { Config } from '@react-router/dev/config'

export default {
  future: {
    v8_middleware: true,
  },
} satisfies Config

On v8, omit the future block entirely β€” the flag no longer exists.

Required: rootAuthLoader must be called in root.tsx's loader. Without it, getAuth throws in nested loaders.

Mental Model

React Router v7/v8 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

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
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

Import Map

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

See Also

  • clerk-setup - Initial Clerk install
  • clerk-custom-ui - Custom flows & appearance
  • clerk-orgs - B2B organizations

Docs

React Router SDK

Source: SKILL.md on GitHub

No alerts16d4 checks Β· Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides safe and well-documented patterns for integrating Clerk authentication with React Router v7/v8, correctly handling server-side authentication and SSR user data through official SDK methods.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW Β· No issues

  • ZeroLeaks5mo

    Score: 93/100 Β· 2 sections analyzed

Signed by skilld at 23e7108. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 20 hours ago.

Activeupdated 3 months ago
What it can do
Network
metadata
{
  "author": "clerk",
  "version": "1.1.0"
}
All 1 allowed tools
WebFetch
  • TypeScript
  • react-router
  • clerk
  • authentication
  • loaders
  • ssr
  • middleware
  • protected-routes

README badge

README badge for clerk/skills/clerk-react-router-patterns

Adds Clerk authentication to React Router v7 apps using rootAuthLoader, getAuth in loaders/actions, clerkMiddleware, and protected routes. Covers SSR user data, org switching, and common setup pitfalls like missing middleware or uninitialized rootAuthLoader.

Generated from the current SKILL.md.

Do I need to call rootAuthLoader in every route file?
No. rootAuthLoader must be called only in root.tsx's loader. Nested loaders use getAuth(args) instead, which reads the auth context that rootAuthLoader sets up.
Can I use getAuth in client components?
No. getAuth is a server-only function for loaders and actions. Use useAuth or useUser hooks in client components instead.
What happens if I forget to export clerkMiddleware?
The middleware won't run on requests, so auth context won't be attached and getAuth will fail or return empty values in loaders.
Does this skill support React Router v6?
No. This skill requires React Router v7.9 or later and @clerk/react-router v3+.

Generated from the current SKILL.md. These answers refresh after source changes.