react-expert
React Expert
Component Structure
- Use functional components over class components
- Keep components small and focused
- Extract reusable logic into custom hooks
- Use composition over inheritance
- Implement proper prop types with TypeScript
- Split large components into smaller, focused ones
Hooks
- Follow the Rules of Hooks
- Use custom hooks for reusable logic
- Keep hooks focused and simple
- Use appropriate dependency arrays in useEffect
- Implement cleanup in useEffect when needed
- Avoid nested hooks
State Management
- Use useState for local component state
- Implement useReducer for complex state logic
- Use Context API for shared state
- Keep state as close to where it's used as possible
- Avoid prop drilling through proper state management
- Use state management libraries only when necessary
Performance
- Use React Compiler (available in React 19) for automatic memoization — remove manual
useMemo/useCallbackwhere the compiler can infer them - Only add
React.memo,useMemo,useCallbackwhen the compiler cannot help (complex object identity, external deps, stable callback refs for third-party libraries) - Avoid unnecessary re-renders; verify with React DevTools Profiler before adding manual memoization
- Implement proper lazy loading with
React.lazyandSuspense - Use proper key props in lists
- Keep components small and focused — small components maximize compiler optimization surface
React 19 Features
React Compiler
- The React Compiler (stable in React 19) performs automatic memoization at build time
- Remove redundant
React.memo,useMemo,useCallbackwrappers — the compiler handles them - Compiler opt-out: add
// @no-react-compilerpragma to a component/file when manual control is needed - Still use
useMemo/useCallbackfor: stable refs passed to third-party libs, expensive computations with external deps the compiler cannot see
Actions
- Use async functions as form
actionprops for automatic pending/error state management - Actions replace the
onSubmit+ manual loading/error state boilerplate pattern - Server Actions (in Next.js / RSC frameworks) allow calling server-side code directly from forms
// Form Action pattern (React 19)
async function saveUser(formData: FormData) {
'use server'; // only in RSC frameworks; omit for client Actions
await db.users.update({ name: formData.get('name') });
}
<form action={saveUser}>
<input name="name" />
<button type="submit">Save</button>
</form>;
useActionState
- Use
useActionStateto track the result and pending state of a form Action - Signature:
const [state, formAction, isPending] = useActionState(fn, initialState) isPendingreplaces the manualuseState(false)loading flag patternstateholds the return value of the last action invocation (success data or error)
import { useActionState } from 'react';
async function submitAction(prevState: State, formData: FormData) {
const result = await saveData(formData);
return result.error ? { error: result.error } : { success: true };
}
function MyForm() {
const [state, formAction, isPending] = useActionState(submitAction, null);
return (
<form action={formAction}>
{state?.error && <p>{state.error}</p>}
<button disabled={isPending}>{isPending ? 'Saving...' : 'Save'}</button>
</form>
);
}
useOptimistic
- Use
useOptimisticfor instant UI feedback before a server response arrives - Signature:
const [optimisticState, setOptimistic] = useOptimistic(value, reducer?) - The optimistic value automatically reverts to the real value when the Action resolves
- Always pair with Actions (the optimistic state is scoped to the Action's lifetime)
import { useOptimistic } from 'react';
function ItemList({ items, addItem }: Props) {
const [optimisticItems, addOptimistic] = useOptimistic(items, (state, newItem) => [
...state,
{ ...newItem, pending: true },
]);
async function action(formData: FormData) {
const newItem = { text: formData.get('text') as string, id: crypto.randomUUID() };
addOptimistic(newItem);
await addItem(newItem);
}
return (
<ul>
{optimisticItems.map(item => (
<li key={item.id} className={item.pending ? 'opacity-50' : ''}>
{item.text}
</li>
))}
<form action={action}>
<input name="text" />
<button>Add</button>
</form>
</ul>
);
}
use() hook
use(promise)— read a Promise's resolved value during render (integrates with Suspense/ErrorBoundary)use(Context)— replacesuseContext; can be called conditionally (unlike other hooks)- Unlike
useEffect,use(promise)does not create a new Promise on each render; pass a stable promise - Use
use(Context)when you need context conditionally or inside loops
import { use } from 'react';
// Reading context conditionally (not possible with useContext)
function Component({ show }: { show: boolean }) {
if (!show) return null;
const theme = use(ThemeContext); // valid — use() can be called conditionally
return <div className={theme.bg}>...</div>;
}
// Reading a promise (wrap in Suspense + ErrorBoundary)
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // suspends until resolved
return <p>{user.name}</p>;
}
Other React 19 API Changes
refis now a plain prop — noforwardRefwrapper needed (function Input({ ref }) { ... })useFormStatus— read the pending/error state of the nearest parent<form>Action- Document Metadata API: render
<title>,<meta>,<link>anywhere in the component tree; React hoists them to<head> startTransitionsupports async functions (Transitions) in React 19useDeferredValuenow accepts aninitialValueparameter for SSR hydrationuseIdstable for server components; use for accessibility IDs (label htmlFor / aria-labelledby)
React Server Components (RSC)
RSC is an architectural boundary, not an optimization toggle. Understand the split before placing components.
Component Classification Rules
- Server Component (default in Next.js App Router): no
useState, nouseEffect, no event handlers, no browser APIs — renders on server only, zero client JS shipped - Client Component (
'use client'directive): interactive, uses hooks, event handlers, browser APIs — hydrates in browser - Mark a component
'use client'at the top of the file; all imports below that boundary are also client-side
Data Fetching Patterns
- Fetch data directly in Server Components using
async/await— nouseEffect, no loading state boilerplate - Co-locate data fetching with the component that needs it (avoid prop drilling fetched data)
- Use
Suspenseboundaries to stream Server Component output progressively
// Server Component — fetch directly, no useEffect
async function UserCard({ userId }: { userId: string }) {
const user = await db.users.findById(userId); // direct DB / API call
return <div>{user.name}</div>;
}
// Client Component — interactive leaf
('use client');
function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(l => !l)}>{liked ? 'Unlike' : 'Like'}</button>;
}
Composition Boundary Rules
- Server Components can render Client Components
- Client Components cannot import Server Components directly — pass Server Components as
childrenprops instead - Keep Client Components as small leaf nodes; push data fetching up into Server Components
// WRONG: importing a Server Component inside a Client Component
'use client'
import { ServerComp } from './ServerComp' // breaks — ServerComp would be bundled client-side
// CORRECT: pass as children prop
'use client'
function ClientShell({ children }: { children: React.ReactNode }) {
return <div onClick={...}>{children}</div>
}
// In a Server Component parent:
<ClientShell><ServerComp /></ClientShell>
Caching and Revalidation (Next.js App Router)
- Use
revalidatePath/revalidateTagin Server Actions to bust cache after mutations - Use
cache()from React to deduplicate fetches within a single render pass - Avoid over-caching: fetch with
{ cache: 'no-store' }for user-specific or real-time data
When NOT to Use RSC
- Highly interactive components (modals, drag-and-drop, real-time) — use Client Components
- Components relying on Web APIs (localStorage, geolocation, canvas) — use Client Components
- When RSC adds complexity without bundle savings — do not force the pattern
Radix UI & Shadcn
- Implement Radix UI components according to documentation
- Follow accessibility guidelines for all components
- Use Shadcn UI conventions for styling
- Compose primitives for complex components
Forms
- Prefer React 19 Actions (
actionprop on<form>) over manualonSubmit+useStateloading boilerplate - Use
useActionStateto track pending, error, and result state from form Actions - Use
useFormStatusinside child components to read the enclosing form's pending state - Use
useOptimisticfor instant feedback during async submissions - Fall back to controlled components (
value+onChange) when fine-grained validation or character-level feedback is required - Use form libraries (React Hook Form, Zod) for complex multi-step forms with schema validation
- Implement proper accessibility: associate labels with
htmlFor, usearia-describedbyfor error messages, manage focus on error
Error Handling
- Implement Error Boundaries
- Handle async errors properly
- Show user-friendly error messages
- Implement proper fallback UI
- Log errors appropriately
Testing
- Write unit tests for components
- Implement integration tests for complex flows
- Use React Testing Library
- Test user interactions
- Test error scenarios
Accessibility
- Use semantic HTML elements
- Implement proper ARIA attributes
- Ensure keyboard navigation
- Test with screen readers
- Handle focus management
- Provide proper alt text for images
Templates
export function Button({ className, children }: ButtonProps) { return (
type State = { error?: string; success?: boolean } | null
async function submitContact(prevState: State, formData: FormData): Promise { try { // perform mutation return { success: true } } catch (err) { return { error: err instanceof Error ? err.message : 'Unknown error' } } }
export function ContactForm() { const [state, formAction, isPending] = useActionState(submitContact, null) return (
// Create the promise OUTSIDE the component (stable reference) function fetchUserProfile(): Promise { return fetch('/api/users').then(r => r.json()) }
export function UserProfileDisplay({ dataPromise }: { dataPromise: Promise }) { const data = use(dataPromise) // suspends until resolved return {/render data/} }
// Usage: <Suspense fallback={}>
interface UseUserDataResult { data: UserData | null loading: boolean error: Error | null }
export function useUserData(): UseUserDataResult { const [data, setData] = useState<UserData | null>(null) const [loading, setLoading] = useState(true) const [error, setError] = useState<Error | null>(null)
useEffect(() => { let cancelled = false async function load() { try { setLoading(true) const result = await fetch('/api/users').then(r => r.json()) if (!cancelled) setData(result) } catch (err) { if (!cancelled) setError(err instanceof Error ? err : new Error('Unknown error')) } finally { if (!cancelled) setLoading(false) } } load() return () => { cancelled = true } }, [])
return { data, loading, error } }
Validation
Iron Laws
- ALWAYS use functional components with hooks — class components are legacy code and incompatible with React Compiler, Server Components, and future concurrent features.
- NEVER violate the Rules of Hooks — hooks must always be called at the top level of a component, never inside conditions, loops, or nested functions.
- ALWAYS push state down to the lowest component that needs it — lifting state unnecessarily causes excessive re-renders and couples unrelated components.
- NEVER perform side effects directly in component render — use
useEffectfor post-render effects or Server Components for async data fetching. - ALWAYS keep Client Components as small leaf nodes — the more code in
'use client'components, the more JavaScript shipped to the browser.
Anti-Patterns
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
| Using class components in new code | Incompatible with React Compiler, Server Components, and concurrent features | Always use functional components with hooks |
| Calling hooks conditionally or in loops | Violates Rules of Hooks; React depends on call order stability across renders | Always call hooks at the top level; use conditions inside the hook body |
Manual useMemo/useCallback everywhere |
Premature optimization; adds noise and complexity without measurable benefit | Profile first; use React Compiler; only memoize when DevTools shows real re-render cost |
Fetching data in useEffect |
Causes request waterfalls, loading flicker, and race conditions | Use Server Components for async fetch; React Query for client-side caching |
Marking large components as 'use client' |
Bundles entire component tree including server data into client JS | Push 'use client' to small interactive leaf components; keep data components as Server |
Memory Protocol (MANDATORY)
Before starting:
cat .claude/context/memory/learnings.md
After completing: Record any new patterns or exceptions discovered.
ASSUME INTERRUPTION: Your context may reset. If it's not in memory, it didn't happen.