.claude/skills/react-patterns/SKILL.md
Modern React patterns for this Next.js 15 application. Covers Server/Client Components, Context API, TanStack Query, Suspense, Error Boundaries, and performance patterns. Use this skill when implementing React components or understanding component architecture.
npx skillsauth add NextSpark-js/nextspark react-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Patterns for working with React 19 and Next.js 15 App Router in this application.
REACT COMPONENT ARCHITECTURE:
Server Components (Default):
├── app/layout.tsx # Root layout with providers
├── app/page.tsx # Pages (default server)
└── app/dashboard/layout.tsx # Nested layouts
Client Components ('use client'):
├── core/components/ # Interactive UI components
├── core/contexts/ # React Context providers
├── core/hooks/ # Custom hooks
└── core/providers/ # Provider wrappers
Data Fetching Strategy:
├── TanStack Query # Client-side data fetching
├── Server Actions # Server mutations
└── Route Handlers # API endpoints
All components in the app/ directory are Server Components by default.
// app/layout.tsx - Server Component
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
// Can use async/await directly
const locale = await getUserLocale()
const messages = await getMessages({ locale })
const defaultTheme = await getDefaultThemeMode()
return (
<html lang={locale} suppressHydrationWarning>
<body>
{/* Wrap client components in providers */}
<NextIntlClientProvider messages={messages}>
<NextThemeProvider defaultTheme={defaultTheme}>
<QueryProvider>
<TeamProvider>
{children}
</TeamProvider>
</QueryProvider>
</NextThemeProvider>
</NextIntlClientProvider>
</body>
</html>
)
}
When to use Server Components:
Mark with 'use client' directive at the top of the file.
'use client'
import { useState, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import { useAuth } from '@/core/hooks/useAuth'
export function LoginForm() {
const [error, setError] = useState<string | null>(null)
const { signIn } = useAuth()
const router = useRouter()
const handleSubmit = useCallback(async (data: LoginData) => {
try {
await signIn(data)
router.push('/dashboard')
} catch (err) {
setError(err instanceof Error ? err.message : 'Sign in failed')
}
}, [signIn, router])
return (
<form onSubmit={handleSubmit}>
{/* Form content */}
</form>
)
}
When to use Client Components:
// core/contexts/TeamContext.tsx
'use client'
import { createContext, useContext, useState, useCallback, ReactNode } from 'react'
interface TeamContextValue {
currentTeam: Team | null
userTeams: UserTeamMembership[]
isLoading: boolean
switchTeam: (teamId: string) => Promise<void>
refreshTeams: () => Promise<void>
}
const TeamContext = createContext<TeamContextValue | undefined>(undefined)
export function TeamProvider({ children }: { children: ReactNode }) {
const [currentTeam, setCurrentTeam] = useState<Team | null>(null)
const [userTeams, setUserTeams] = useState<UserTeamMembership[]>([])
const [isLoading, setIsLoading] = useState(true)
const switchTeam = useCallback(async (teamId: string) => {
// Implementation
}, [])
const refreshTeams = useCallback(async () => {
// Implementation
}, [])
return (
<TeamContext.Provider value={{
currentTeam,
userTeams,
isLoading,
switchTeam,
refreshTeams,
}}>
{children}
</TeamContext.Provider>
)
}
// Custom hook with error boundary
export function useTeamContext() {
const context = useContext(TeamContext)
if (context === undefined) {
throw new Error('useTeamContext must be used within TeamProvider')
}
return context
}
// app/layout.tsx
<NextIntlClientProvider messages={messages}>
<NextThemeProvider>
<QueryProvider>
<TeamProvider>
<SubscriptionProvider>
{children}
</SubscriptionProvider>
</TeamProvider>
</QueryProvider>
</NextThemeProvider>
</NextIntlClientProvider>
Order matters: Providers that depend on others must be nested inside them.
// core/providers/query-provider.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { useState } from 'react'
export function QueryProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
refetchOnWindowFocus: false,
},
},
})
)
return (
<QueryClientProvider client={queryClient}>
{children}
{process.env.NEXT_PUBLIC_RQ_DEVTOOLS === 'true' && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</QueryClientProvider>
)
}
// ✅ CORRECT - Use TanStack Query for data fetching
import { useQuery } from '@tanstack/react-query'
function CustomerList() {
const { data, isLoading, error } = useQuery({
queryKey: ['customers'],
queryFn: () => fetch('/api/v1/customers').then(res => res.json()),
})
if (isLoading) return <Skeleton />
if (error) return <ErrorMessage error={error} />
return <DataTable data={data} />
}
// ❌ WRONG - Don't use useEffect for data fetching
function CustomerList() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/v1/customers')
.then(res => res.json())
.then(setData)
.finally(() => setLoading(false))
}, [])
}
import { useMutation, useQueryClient } from '@tanstack/react-query'
function CreateCustomerForm() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (data: CreateCustomer) =>
fetch('/api/v1/customers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
}).then(res => res.json()),
onSuccess: () => {
// Invalidate queries to refetch
queryClient.invalidateQueries({ queryKey: ['customers'] })
},
})
return (
<form onSubmit={(e) => {
e.preventDefault()
mutation.mutate(formData)
}}>
{/* Form fields */}
</form>
)
}
import { Suspense } from 'react'
function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div>
<Suspense fallback={<NavSkeleton />}>
<Navigation />
</Suspense>
<main>
<Suspense fallback={<ContentSkeleton />}>
{children}
</Suspense>
</main>
</div>
)
}
// app/dashboard/layout.tsx
'use client'
import { Suspense } from 'react'
function AuthMethodDetectorWrapper() {
useAuthMethodDetector() // Hook that reads URL params
return null
}
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<>
{/* Wrap async detection in Suspense */}
<Suspense fallback={null}>
<AuthMethodDetectorWrapper />
</Suspense>
<div data-cy="dashboard-container">
{children}
</div>
</>
)
}
// app/components/page-renderer.tsx
import { Suspense } from 'react'
function BlockSkeleton() {
return (
<div className="w-full py-12 px-4 animate-pulse">
<div className="h-8 bg-muted rounded w-1/3 mb-4" />
<div className="h-4 bg-muted rounded w-2/3" />
</div>
)
}
function BlockRenderer({ block }: { block: BlockInstance }) {
const BlockComponent = getBlockComponent(block.blockSlug)
return (
<Suspense fallback={<BlockSkeleton />}>
<BlockComponent {...block.props} />
</Suspense>
)
}
// app/dashboard/(main)/[entity]/error.tsx
'use client'
import { useEffect } from 'react'
import { Button } from '@/core/components/ui/button'
import { AlertCircle } from 'lucide-react'
export default function EntityError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error('Entity page error:', error)
}, [error])
return (
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4 p-6">
<AlertCircle className="h-12 w-12 text-destructive" />
<h2 className="text-xl font-semibold">Something went wrong!</h2>
<p className="text-sm text-muted-foreground max-w-md text-center">
{error.message || 'An error occurred while loading this page.'}
</p>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => window.history.back()}>
Go Back
</Button>
<Button onClick={() => reset()}>
Try Again
</Button>
</div>
{/* Development error details */}
{process.env.NODE_ENV === 'development' && error.stack && (
<details className="mt-4 p-4 bg-muted rounded-lg text-xs max-w-2xl w-full">
<summary className="cursor-pointer font-medium">Error Details</summary>
<pre className="mt-2 overflow-auto">{error.stack}</pre>
</details>
)}
</div>
)
}
// Block error fallback
function BlockError({ blockSlug }: { blockSlug: string }) {
return (
<div className="w-full py-12 px-4 bg-destructive/10 border border-destructive/20 rounded">
<div className="max-w-7xl mx-auto text-center">
<p className="text-destructive">
Failed to load block: <code className="font-mono">{blockSlug}</code>
</p>
<p className="text-sm text-muted-foreground mt-2">
This block may not be available or there was an error rendering it.
</p>
</div>
</div>
)
}
'use client'
import { useCallback, useState } from 'react'
function ThemeToggle() {
const { setTheme, theme } = useTheme()
const { user } = useAuth()
// Memoize callback to prevent unnecessary re-renders
const handleThemeChange = useCallback(async (newTheme: string) => {
setTheme(newTheme)
// Persist preference if logged in
if (user?.id) {
try {
await fetch('/api/user/profile', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
meta: { uiPreferences: { theme: newTheme } }
}),
})
} catch (error) {
console.error('Failed to save theme preference:', error)
}
}
}, [setTheme, user])
return (
<DropdownMenu>
<DropdownMenuItem onClick={() => handleThemeChange('light')}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleThemeChange('dark')}>
Dark
</DropdownMenuItem>
</DropdownMenu>
)
}
// ✅ CORRECT - useState with initializer function
const [queryClient] = useState(() => new QueryClient())
// ❌ WRONG - Creates new instance on every render
const queryClient = new QueryClient()
// core/hooks/useAuth.ts
'use client'
import { useRouter } from 'next/navigation'
import { authClient } from '@/core/lib/auth-client'
export function useAuth() {
const router = useRouter()
const session = authClient.useSession()
const handleSignIn = async ({ email, password, redirectTo }: SignInParams) => {
const { data, error } = await authClient.signIn.email({ email, password })
if (error) {
throw new Error(error.message || 'Error signing in')
}
if (data) {
router.push(redirectTo || '/dashboard')
}
return data
}
const handleSignOut = async () => {
if (typeof window !== 'undefined') {
localStorage.removeItem('activeTeamId')
}
await authClient.signOut()
router.push('/login')
}
return {
user: session.data?.user,
session: session.data,
isLoading: session.isPending,
signIn: handleSignIn,
signOut: handleSignOut,
// ... other auth methods
}
}
These patterns are documented in .rules/components.md but not currently used in the codebase:
// Pattern available in React 19
import { use } from 'react'
function TodoList({ todosPromise }: { todosPromise: Promise<Todo[]> }) {
const todos = use(todosPromise) // Suspends until resolved
return (
<ul>
{todos.map(todo => <li key={todo.id}>{todo.title}</li>)}
</ul>
)
}
Current approach: Use TanStack Query instead.
// Pattern available in React 19
import { useActionState } from 'react'
function LoginForm() {
const [state, formAction, isPending] = useActionState(loginAction, null)
return (
<form action={formAction}>
{state?.error && <p className="text-destructive">{state.error}</p>}
<input name="email" type="email" />
<input name="password" type="password" />
<button disabled={isPending}>Sign In</button>
</form>
)
}
Current approach: Use React Hook Form with Zod validation.
// ❌ NEVER: Use namespace import for React
import * as React from 'react'
React.useState()
React.useCallback()
React.memo()
// ✅ CORRECT: Use named imports (project convention)
import { useState, useCallback, memo } from 'react'
// For types: import { type MouseEvent, type DragEvent } from 'react'
// ❌ NEVER: Use useEffect for data fetching
useEffect(() => {
fetch('/api/data').then(setData)
}, [])
// ✅ CORRECT: Use TanStack Query
const { data } = useQuery({ queryKey: ['data'], queryFn: fetchData })
// ❌ NEVER: Create objects/functions in render without memoization
<Button onClick={() => handleClick(id)} /> // New function each render
// ✅ CORRECT: Use useCallback for stable references
const handleButtonClick = useCallback(() => handleClick(id), [id])
<Button onClick={handleButtonClick} />
// ❌ NEVER: Access hooks conditionally
if (condition) {
const value = useContext(MyContext) // Breaks Rules of Hooks
}
// ✅ CORRECT: Always call hooks at top level
const value = useContext(MyContext)
if (condition) {
// use value
}
// ❌ NEVER: Forget 'use client' directive for client components
export function InteractiveComponent() {
const [state, setState] = useState() // Error: useState is not defined
}
// ✅ CORRECT: Add directive at top of file
'use client'
export function InteractiveComponent() {
const [state, setState] = useState()
}
// ❌ NEVER: Use client-side hooks in Server Components
// app/page.tsx (Server Component)
export default function Page() {
const [count, setCount] = useState(0) // Error!
}
// ✅ CORRECT: Extract to Client Component
// app/page.tsx
import { Counter } from './Counter'
export default function Page() {
return <Counter /> // Client Component handles state
}
Before finalizing React component implementation:
import * as React)tanstack-query - Data fetching patternsshadcn-components - UI component patternsaccessibility - ARIA and keyboard patternscypress-selectors - Testing selectorsdevelopment
Zod validation patterns for this Next.js application. Covers schema definition, API validation, form integration, error formatting, and type inference. Use this skill when implementing validation for APIs, forms, or entity schemas.
development
Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
testing
Test coverage metrics and registry system for this Next.js application. Covers FEATURE_REGISTRY, FLOW_REGISTRY, TAGS_REGISTRY, and coverage metrics interpretation. Use this skill when evaluating test coverage, identifying gaps, or planning testing priorities.
development
TanStack Query (React Query) patterns for data fetching in this Next.js application. Covers useQuery, useMutation, optimistic updates, cache invalidation, and anti-patterns. Use this skill when implementing data fetching or state management with server data.