SKILL DETAIL
clerk-react-patterns
clerk/skills/clerk-react-patterns
This skill provides comprehensive patterns for integrating Clerk authentication into React single-page applications built with Vite or Create React App, using the @clerk/react package. It covers the initial setup of ClerkProvider, the usage of core hooks like useAuth, useUser, and useClerk, and how to implement protected routes with React Router. Additionally, it explains how to build custom sign-in and sign-up forms, and how to obtain session tokens for API calls. The skill emphasizes the client-only nature of @clerk/react, where all auth state comes from hooks, and highlights the importance of checking isLoaded before trusting isSignedIn. Through reference documents, developers can quickly find solutions for common tasks and avoid pitfalls such as incorrect environment variable prefixes or missing Provider components.
Installation
npx skills add https://github.com/clerk/skills --skill clerk-react-patterns
스킬 파일
SKILL.md
최근 동기화 · 2026. 9. 13.
evals/evals.json›
{
"skill_name": "clerk-react-patterns",
"evals": [
{
"id": 1,
"prompt": "i created a new vite react app and want to add clerk authentication. show me how to set up ClerkProvider and add sign-in and sign-out buttons to my app.",
"expected_output": "Wraps app in ClerkProvider in main.tsx with publishableKey from VITE_CLERK_PUBLISHABLE_KEY, adds Show/SignInButton/UserButton to App.tsx",
"scaffold": "react-basic-auth",
"expectations": [
"Adds ClerkProvider to src/main.tsx wrapping the App component",
"Reads publishableKey from import.meta.env.VITE_CLERK_PUBLISHABLE_KEY",
"Uses Show when='signed-in' and Show when='signed-out' for conditional rendering",
"Adds UserButton for signed-in state and SignInButton for signed-out",
".env file uses VITE_ prefix not NEXT_PUBLIC_"
]
},
{
"id": 2,
"prompt": "i have a react spa with react router v6. i need to protect my /dashboard and /settings routes so only authenticated users can access them. unauthorized users should be redirected to /sign-in.",
"expected_output": "Creates ProtectedRoute component using useAuth with isLoaded and isSignedIn checks, wraps protected routes in React Router",
"scaffold": "react-basic-auth",
"expectations": [
"Creates a ProtectedRoute wrapper component",
"Uses useAuth() with isLoaded and isSignedIn checks",
"Shows loading state while isLoaded is false",
"Redirects to /sign-in using <Navigate replace> when not signed in",
"Wraps /dashboard and /settings using the ProtectedRoute in the router config"
]
},
{
"id": 3,
"prompt": "i want to build a custom sign-in form with email and password fields instead of using the hosted sign-in page. use clerk's useSignIn hook.",
"expected_output": "Custom sign-in form using useSignIn() hook with signIn.password() or signIn.create(), then finalize() or setActive()",
"scaffold": "react-basic-auth",
"expectations": [
"Uses useSignIn hook from @clerk/react",
"Calls signIn.password({ identifier, password }) or signIn.create({ identifier, password }) to authenticate",
"Checks for success before activating the session (via status check, error check, or conditional after the auth call)",
"Activates the session using signIn.finalize() or setActive({ session: createdSessionId })",
"Catches and displays error messages from the sign-in response"
]
},
{
"id": 4,
"prompt": "i want a user profile page that shows the signed-in user's name, email address, and profile picture. use clerk hooks to get this data.",
"expected_output": "Profile page using useUser() hook accessing user.firstName, user.emailAddresses[0], user.imageUrl",
"scaffold": "react-basic-auth",
"expectations": [
"Uses useUser() hook from @clerk/react",
"Guards on isLoaded before rendering user data",
"Accesses user.firstName and user.lastName for name",
"Accesses user.emailAddresses[0].emailAddress for email",
"Displays user.imageUrl in an img element"
]
},
{
"id": 5,
"prompt": "i need to add an organization switcher to my react spa so users can switch between their orgs. how do i render the switcher and read the active org id?",
"expected_output": "Renders OrganizationSwitcher from @clerk/react and reads orgId from useAuth()",
"scaffold": "react-basic-auth",
"expectations": [
"Imports and renders <OrganizationSwitcher /> from @clerk/react",
"Uses orgId from useAuth() to get the active organization ID",
"Handles the case where orgId is null (no org selected)",
"Places the switcher in a visible location (nav, header, or sidebar)",
"Imports from @clerk/react not @clerk/nextjs"
]
},
{
"id": 6,
"prompt": "my react app needs to call a protected backend API. how do i get the clerk session token to include in the Authorization header?",
"expected_output": "Uses getToken() from useAuth() and includes the token as Bearer in the Authorization header",
"scaffold": "react-basic-auth",
"expectations": [
"Uses getToken() from useAuth() hook",
"Awaits the getToken() call (it returns a Promise)",
"Null-checks the token before using it",
"Sets Authorization: Bearer <token> header on the fetch request",
"Shows a pattern for reuse (custom hook or utility function)"
]
}
]
}
references/custom-flows.md›
# Custom Flows (HIGH)
## Custom Sign-In with useSignIn
```tsx
import { useSignIn } from '@clerk/react'
import { useNavigate } from 'react-router-dom'
import { useState } from 'react'
export function CustomSignIn() {
const { isLoaded, signIn, setActive } = useSignIn()
const navigate = useNavigate()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!isLoaded) return
try {
const result = await signIn.create({
identifier: email,
password,
})
if (result.status === 'complete') {
await setActive({ session: result.createdSessionId })
navigate('/dashboard')
}
} catch (err: unknown) {
const clerkError = err as { errors?: Array<{ message: string }> }
setError(clerkError.errors?.[0]?.message ?? 'Sign in failed')
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
placeholder="Password"
/>
{error && <p>{error}</p>}
<button type="submit">Sign In</button>
</form>
)
}
```
## Custom Sign-Up with useSignUp
```tsx
import { useSignUp } from '@clerk/react'
export function CustomSignUp() {
const { isLoaded, signUp, setActive } = useSignUp()
async function handleSubmit(email: string, password: string) {
if (!isLoaded) return
const result = await signUp.create({ emailAddress: email, password })
if (result.status === 'complete') {
await setActive({ session: result.createdSessionId })
} else if (result.status === 'missing_requirements') {
// Email verification needed
await signUp.prepareEmailAddressVerification({ strategy: 'email_code' })
}
}
}
```
## Email Verification
```tsx
async function verifyEmail(code: string) {
const result = await signUp.attemptEmailAddressVerification({ code })
if (result.status === 'complete') {
await setActive({ session: result.createdSessionId })
navigate('/dashboard')
}
}
```
## signIn.status Values
| Status | Meaning |
|--------|---------|
| `'complete'` | Auth successful, call `setActive` |
| `'needs_first_factor'` | First factor required (e.g., password) |
| `'needs_second_factor'` | MFA required |
| `'needs_new_password'` | Password reset required |
Always check `result.status === 'complete'` before calling `setActive`.
[Docs](https://clerk.com/docs/react/getting-started/quickstart)
references/hooks.md›
# Hooks (CRITICAL)
## isLoaded Guard
Always check `isLoaded` before trusting auth state. Rendering before Clerk initializes gives wrong results:
```tsx
import { useAuth } from '@clerk/react'
export function Page() {
const { isLoaded, isSignedIn, userId } = useAuth()
if (!isLoaded) return <div>Loading...</div>
if (!isSignedIn) return <div>Please sign in</div>
return <div>Hello {userId}</div>
}
```
## useAuth()
Returns auth primitives:
| Property | Type | Description |
|----------|------|-------------|
| `isLoaded` | `boolean` | `false` while Clerk initializes |
| `isSignedIn` | `boolean \| undefined` | `undefined` until `isLoaded` is true |
| `userId` | `string \| null` | Current user ID |
| `sessionId` | `string \| null` | Current session ID |
| `orgId` | `string \| null` | Active organization ID |
| `orgRole` | `string \| null` | Active org role |
| `getToken` | `Function` | Fetches session JWT |
| `signOut` | `Function` | Signs the user out |
## useUser()
Returns full user profile:
```tsx
import { useUser } from '@clerk/react'
export function Profile() {
const { isLoaded, isSignedIn, user } = useUser()
if (!isLoaded || !isSignedIn) return null
return (
<div>
<img src={user.imageUrl} alt={user.fullName ?? ''} />
<p>{user.firstName} {user.lastName}</p>
<p>{user.emailAddresses[0]?.emailAddress}</p>
</div>
)
}
```
## useClerk()
Access the Clerk instance for programmatic control:
```tsx
import { useClerk } from '@clerk/react'
export function NavBar() {
const { signOut, openUserProfile, openOrganizationProfile } = useClerk()
return (
<nav>
<button onClick={() => openUserProfile()}>Profile</button>
<button onClick={() => signOut()}>Sign out</button>
</nav>
)
}
```
## getToken() for API Calls
```tsx
import { useAuth } from '@clerk/react'
export function useAuthFetch() {
const { getToken } = useAuth()
return async function authFetch(url: string, init?: RequestInit) {
const token = await getToken()
if (!token) throw new Error('Not authenticated')
return fetch(url, {
...init,
headers: {
...init?.headers,
Authorization: `Bearer ${token}`,
},
})
}
}
```
`getToken()` returns `null` when unauthenticated — always null-check.
[Docs](https://clerk.com/docs/react/getting-started/quickstart)
references/protected-routes.md›
# Protected Routes (CRITICAL)
## ProtectedRoute Component
Wrap protected routes with a guard component:
```tsx
import { Navigate, Outlet } from 'react-router-dom'
import { useAuth } from '@clerk/react'
export function ProtectedRoute() {
const { isLoaded, isSignedIn } = useAuth()
if (!isLoaded) return <div>Loading...</div>
if (!isSignedIn) return <Navigate to="/sign-in" replace />
return <Outlet />
}
```
## Router Setup (React Router v6/v7)
```tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { ClerkProvider } from '@clerk/react'
import { ProtectedRoute } from './ProtectedRoute'
import { Dashboard } from './Dashboard'
import { Settings } from './Settings'
import { SignInPage } from './SignInPage'
function App() {
return (
<ClerkProvider publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}>
<BrowserRouter>
<Routes>
<Route path="/sign-in" element={<SignInPage />} />
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Route>
</Routes>
</BrowserRouter>
</ClerkProvider>
)
}
```
`ClerkProvider` must be an ancestor of both the router and any Clerk hooks/components.
## Org-Gated Route
```tsx
import { Navigate, Outlet } from 'react-router-dom'
import { useAuth } from '@clerk/react'
export function OrgRoute() {
const { isLoaded, isSignedIn, orgId } = useAuth()
if (!isLoaded) return <div>Loading...</div>
if (!isSignedIn) return <Navigate to="/sign-in" replace />
if (!orgId) return <Navigate to="/select-org" replace />
return <Outlet />
}
```
## Using Prebuilt Components
For simpler cases, wrap with `<SignedIn>` / `<SignedOut>` equivalents:
```tsx
import { Show } from '@clerk/react'
import { Navigate } from 'react-router-dom'
export function Dashboard() {
return (
<Show when="signed-in" fallback={<Navigate to="/sign-in" replace />}>
<DashboardContent />
</Show>
)
}
```
The `ProtectedRoute` pattern is preferred for route-level control; `<Show>` is better for component-level.
[Docs](https://clerk.com/docs/react/getting-started/quickstart)
references/router-integration.md›
# Router Integration (HIGH)
## ClerkProvider Position
Place `ClerkProvider` outside the router but inside `StrictMode`:
```tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import { ClerkProvider } from '@clerk/react'
import App from './App'
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ClerkProvider publishableKey={PUBLISHABLE_KEY}>
<BrowserRouter>
<App />
</BrowserRouter>
</ClerkProvider>
</StrictMode>,
)
```
`ClerkProvider` must wrap `BrowserRouter` so hooks work inside route components.
## Redirect URLs
Configure where users land after sign-in/sign-up:
```tsx
<ClerkProvider
publishableKey={PUBLISHABLE_KEY}
afterSignInUrl="/dashboard"
afterSignUpUrl="/onboarding"
>
```
Or via environment variables:
```
VITE_CLERK_SIGN_IN_URL=/sign-in
VITE_CLERK_SIGN_UP_URL=/sign-up
VITE_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/dashboard
VITE_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/dashboard
```
## Using Prebuilt Sign-In Component
```tsx
import { SignIn } from '@clerk/react'
export function SignInPage() {
return (
<div style={{ display: 'flex', justifyContent: 'center', paddingTop: '4rem' }}>
<SignIn />
</div>
)
}
```
Register the route:
```tsx
<Route path="/sign-in/*" element={<SignInPage />} />
<Route path="/sign-up/*" element={<SignUpPage />} />
```
The `/*` wildcard is required for Clerk's multi-step flows (email verification, etc.) to work with React Router.
## useNavigate with Clerk Actions
```tsx
import { useClerk } from '@clerk/react'
import { useNavigate } from 'react-router-dom'
export function SignOutButton() {
const { signOut } = useClerk()
const navigate = useNavigate()
return (
<button onClick={() => signOut(() => navigate('/'))}>
Sign out
</button>
)
}
```
Pass a callback to `signOut` to navigate after the sign-out completes.
[Docs](https://clerk.com/docs/react/getting-started/quickstart)
SKILL.md›
---
name: clerk-react-patterns
description: 'React SPA auth patterns with @clerk/react for Vite/CRA - ClerkProvider
setup, useAuth/useUser/useClerk hooks, React Router protected routes, custom sign-in
flows. Triggers on: Vite Clerk setup, React Router auth, useAuth hook, protected
route, custom sign-in form React.'
license: MIT
allowed-tools: WebFetch
metadata:
author: clerk
version: 1.0.0
---
# React SPA Patterns
> This skill covers `@clerk/react` for Vite/CRA SPAs. For Next.js use `clerk-nextjs-patterns`. For TanStack Start use `clerk-tanstack-patterns`.
## What Do You Need?
| Task | Reference |
|------|-----------|
| useAuth / useUser / useClerk hooks | references/hooks.md |
| Protected routes with React Router | references/protected-routes.md |
| Custom sign-in / sign-up forms | references/custom-flows.md |
| React Router v6/v7 integration | references/router-integration.md |
## References
| Reference | Description |
|-----------|-------------|
| `references/hooks.md` | useAuth, isLoaded guard |
| `references/protected-routes.md` | ProtectedRoute pattern |
| `references/custom-flows.md` | useSignIn, useSignUp flows |
| `references/router-integration.md` | React Router v6/v7 setup |
## Setup
```
npm install @clerk/react
```
`.env`:
```
VITE_CLERK_PUBLISHABLE_KEY=pk_...
```
`src/main.tsx`:
```tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { ClerkProvider } from '@clerk/react'
import App from './App.tsx'
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ClerkProvider publishableKey={PUBLISHABLE_KEY}>
<App />
</ClerkProvider>
</StrictMode>,
)
```
## Mental Model
`@clerk/react` is client-only — there is no server-side `auth()`. All auth state comes from hooks.
- `isLoaded` must be `true` before trusting `isSignedIn` — always guard on `isLoaded`
- `useClerk()` gives access to `signOut`, `openSignIn`, `openUserProfile` and other methods
- `getToken()` from `useAuth()` fetches the session JWT for API calls
## Minimal Pattern
```tsx
import { useAuth } from '@clerk/react'
export function Dashboard() {
const { isLoaded, isSignedIn, userId } = useAuth()
if (!isLoaded) return <div>Loading...</div>
if (!isSignedIn) return <div>Please sign in</div>
return <div>Hello {userId}</div>
}
```
## Protected Route (React Router v6/v7)
```tsx
import { Navigate, Outlet } from 'react-router-dom'
import { useAuth } from '@clerk/react'
export function ProtectedRoute() {
const { isLoaded, isSignedIn } = useAuth()
if (!isLoaded) return <div>Loading...</div>
if (!isSignedIn) return <Navigate to="/sign-in" replace />
return <Outlet />
}
```
```tsx
<Routes>
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Route>
<Route path="/sign-in" element={<SignIn />} />
</Routes>
```
## Token for API Calls
```tsx
import { useAuth } from '@clerk/react'
export function DataFetcher() {
const { getToken } = useAuth()
async function fetchData() {
const token = await getToken()
if (!token) return
const res = await fetch('/api/data', {
headers: { Authorization: `Bearer ${token}` },
})
return res.json()
}
return <button onClick={fetchData}>Load</button>
}
```
## Common Pitfalls
| Symptom | Cause | Fix |
|---------|-------|-----|
| `isSignedIn` is `undefined` | `isLoaded` is still `false` | Always check `isLoaded` first |
| `ClerkProvider` missing | Provider not at root | Wrap `<App>` in `main.tsx` |
| Env var undefined | Wrong Vite prefix | Use `VITE_CLERK_PUBLISHABLE_KEY`, access via `import.meta.env` |
| Token is `null` | User not signed in | Null-check `getToken()` result |
| Sign-in component shows blank | No `publishableKey` on provider | Pass `publishableKey` explicitly |
## See Also
- `clerk-setup` - Initial Clerk install
- `clerk-custom-ui` - Custom flows & appearance
- `clerk-orgs` - B2B organizations
## Docs
[React SDK](https://clerk.com/docs/react/getting-started/quickstart)
templates/react-basic-auth/index.html›
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clerk + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
templates/react-basic-auth/package.json›
{
"name": "clerk-react",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build"
},
"dependencies": {
"@clerk/react": "^5.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.0.0",
"typescript": "^5.6.0",
"vite": "^6.0.0"
}
}
templates/react-basic-auth/src/App.tsx›
import { Show, SignInButton, SignUpButton, UserButton } from "@clerk/react"
function App() {
return (
<>
<header>
<Show when="signed-out">
<SignInButton />
<SignUpButton />
</Show>
<Show when="signed-in">
<UserButton />
</Show>
</header>
</>
)
}
export default App
templates/react-basic-auth/src/main.tsx›
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import App from "./App.tsx"
import { ClerkProvider } from "@clerk/react"
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ClerkProvider>
<App />
</ClerkProvider>
</StrictMode>,
)
templates/react-basic-auth/tsconfig.json›
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}
templates/react-basic-auth/vite.config.ts›
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [react()],
})