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.


name: clerk-react-router-patterns description: 'React Router v7 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.' license: MIT allowed-tools: WebFetch metadata: author: clerk version: 1.0.0

React Router Patterns

SDK: @clerk/react-router v3+. Requires React Router v7.9+.

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

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 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
clerkMiddleware() not detectedMissing middlewareExport middleware = [clerkMiddleware()] from root route
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
ClerkApp@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