Labsco
clerk logo

clerk-react-router-patterns

★ 54

by clerk · part of clerk/skills

React Router v7 patterns with Clerk — rootAuthLoader, getAuth in loaders,

🔥🔥🔥🔥✓ VerifiedFreeQuick setup
🧩 One of 7 skills in the clerk/skills package — works on its own, and pairs well with its siblings.

This is the playbook your agent receives when the skill activates — you don't need to read it to use the skill, but it's here to audit before installing.

React Router Patterns

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

What Do You Need?

TaskReference
Auth in loaders and actionsreferences/loaders-actions.md
Protected routes and redirectsreferences/protected-routes.md
SSR user data and sessionreferences/ssr-auth.md

React Router v7 vs v8

Check the installed react-router major version before scaffolding — the config differs:

v7.9+v8+
Middleware APIOpt-in: set future: { v8_middleware: true } in react-router.config.tsAlways on — do NOT set the flag (v8 removed it)
ssr.noExternal workaround (below)Not neededRequired

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

SymptomCauseFix
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 exportedClerkApp does not exist in @clerk/react-routerUse <ClerkProvider loaderData={loaderData}> in root.tsx's default export
clerkMiddleware() not detectedMissing 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 upgradingRemove the future.v8_middleware entry — middleware is always on in v8
getAuth returns empty userIdrootAuthLoader not calledCall rootAuthLoader(args) in root.tsx loader
Infinite redirect loopRedirect target is also protectedExclude /sign-in from protection check
redirect not working in actionUsing Response instead of throw redirect()Use throw redirect('/path') from react-router

Import Map

WhatImport 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