
React
- 3 installs
- 19 repo stars
- Updated August 1, 2026
- xobotyi/cc-foundry
Helps with frontend development tasks.
About
react is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted development.
- react
- Frontend Development
- AI-coding skill
React by the numbers
- 3 all-time installs (skills.sh)
- Ranked #1,841 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/xobotyi/cc-foundry --skill reactAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 19 |
| Last updated | August 1, 2026 |
| Repository | xobotyi/cc-foundry ↗ |
What it does
Helps with frontend development tasks.
Files
React
Components are pure functions. State is minimal. Effects are escape hatches. If you reach for useEffect, verify you actually need it.
React rewards thinking in components: break UI into pieces, find minimal state, identify where it lives, and wire data flow from parent to child. References contain extended examples, rationale, and edge cases for each topic area.
References
| Topic | Reference | Contents |
|---|---|---|
| Components | ${CLAUDE_SKILL_DIR}/references/components.md | Composition, refs, metadata, custom elements |
| Hooks | ${CLAUDE_SKILL_DIR}/references/hooks.md | Hook rules, custom hooks, useSyncExternalStore |
| State | ${CLAUDE_SKILL_DIR}/references/state.md | Placement, reducers, context, actions |
| Performance | ${CLAUDE_SKILL_DIR}/references/performance.md | Compiler, memoization, server components, streaming |
| Testing | ${CLAUDE_SKILL_DIR}/references/testing.md | Query priority/variants, userEvent catalog, async patterns |
Component Design
Thinking in React
Build UI in five steps:
1. Break UI into a component hierarchy — each component does one thing. 2. Build a static version first — props only, no state, no interactivity. 3. Find minimal state — if it doesn't change, is passed from a parent, or can be computed, it is not state. 4. Identify where state lives — find every component that renders based on the state, find their closest common parent, put state there. 5. Add inverse data flow — pass state-setter callbacks down so children update parent state through event handlers.
Purity
React assumes every component is a pure function. Same props + same state = same JSX. Never mutate props, state, or variables declared before rendering.
- Local mutation is fine. Creating and mutating objects/arrays within the same render
is safe — the mutation is invisible outside that render.
- Event handlers don't need to be pure — they run outside of rendering.
| Side effect type | Where it belongs |
|---|---|
| User clicks, form submits | Event handlers |
| Sync with external system (DOM, network) | useEffect (last resort) |
| Data transformation | Compute during render |
| Shared logic between handlers | Extract a function, call from handlers |
Component Structure
- One component per file. Small helpers co-located in the same file are acceptable but
extract when reused.
- Always prefer function declarations for components.
- Do not use
React.FC— it adds implicitchildrentyping and complicates generics.
Use function Component(props: Props).
Component Body Organization
Separate logic from rendering. The component body handles computation, state, and handler definitions. JSX is declarative — it references results, not processes.
- Handler object — group all event handlers in a single
handleobject. This creates
a clear boundary between logic and rendering:
const handle = {
submit() { /* ... */ },
inputChange(e: ChangeEvent<HTMLInputElement>) { setName(e.target.value); },
keyDown(e: KeyboardEvent) { if (e.key === 'Enter') handle.submit(); },
};Reference in JSX as onChange={handle.inputChange}. Never inline handler logic in JSX.
- Pre-render computation — move list rendering and derived JSX out of the return
statement into component body variables:
const tabElements: ReactNode[] = [];
for (const tab of allTabs) {
tabElements.push(<Tab key={tab.id}>{tab.name}</Tab>);
}
return <TabList>{tabElements}</TabList>;JSX .map() inside the return statement is discouraged — compute element arrays in the body, reference them in JSX.
- Conditional rendering — simple conditions (
{isVisible && <Component />}) are
acceptable inline in JSX. When the condition is complex or involves multiple branches, compute the result in the component body and reference the variable in JSX.
Composition
- Props flow down, events flow up. One-way data flow. Children never mutate parent
state directly — they call callbacks.
- Composition over configuration. Pass JSX as
childrenor render props instead of
building components with dozens of boolean flags.
- When a wrapper component updates its own state, React knows its
childrenprops
haven't changed, so children skip re-rendering.
- Use compound components (shared context between related sub-components) for complex
UI patterns like flyout menus, tabs, accordions.
- Prefer controlled components when parent needs to coordinate state across siblings.
Prefer uncontrolled for isolated, self-contained UI.
Refs
refis a prop. Passrefdirectly as a prop to function components.
Never use forwardRef — it is deprecated.
- Ref callbacks can return a cleanup function, called when the element unmounts.
- Avoid implicit returns in ref callbacks — use block body
{}not parentheses to
prevent TypeScript confusion.
Document Metadata
Render <title>, <meta>, <link> directly in components. React hoists them to <head> automatically. Works with client-only apps, streaming SSR, and Server Components.
Custom Elements
React provides full custom element support. Server rendering: primitive props render as attributes, non-primitive props are omitted. Client rendering: props matching element instance properties are assigned as properties, others as attributes.
JSX Conventions
- Self-closing tags for components without children:
<Input />. - Boolean attributes without value:
<Input disabled />notdisabled={true}. - Fragments to avoid wrapper divs:
<>...</>or<Fragment key={id}>. - Avoid
&&with numbers —count && <List />renders0. Usecount > 0 && <List />
or a ternary.
- Never inline handler logic in JSX. Group all handlers in a
handleobject in the
component body (see Component Body Organization).
- Spread props sparingly —
{...props}makes it unclear what a component accepts.
Prefer explicit props.
keyon every list item. Stable, unique identifiers. Never use array index as key
when items can reorder.
- No side effects during render. Event handlers for user actions, Effects for
synchronization, render for pure computation.
Hooks
use() — Context and Promises
- Prefer
use(MyContext)overuseContext(MyContext).use()can be called inside
conditionals and loops — useContext() cannot.
use()always looks for the closest provider above the calling component.use(promise)integrates with Suspense and Error Boundaries to read promise values.- Do not create promises inside Client Components during render — they recreate every
render. Pass promises from Server Components or use a Suspense-compatible library.
- In Server Components, prefer
async/awaitoveruse(). use()cannot be called in a try-catch block. Use Error Boundaries or
promise.catch() instead.
Hook Rules
- Top level only. Never call hooks inside conditions, loops, or nested functions.
React relies on call order. Exception: use() can be called conditionally.
- React functions only. Call hooks from function components or custom hooks — never
from regular JavaScript functions.
- Exhaustive deps. Include all reactive values used inside the Effect in the dependency
array. The linter enforces this — don't suppress it. If a dependency causes unwanted re-runs, restructure the code.
- One Effect per concern. Don't merge unrelated sync logic into a single Effect.
Separate Effects for separate external systems.
Effects Are Escape Hatches
Use Effects only to synchronize with external systems (DOM APIs, network, browser events). Not for transforming data, handling user events, or state derivation.
You don't need an Effect for:
| Situation | Do this instead |
|---|---|
| Transform data for rendering | Compute during render |
| Handle user events | Call in event handler |
| Reset state on prop change | Use key={userId} on the component |
| Adjust state on prop change | Compute: items.find(...) during render |
| Notify parent of state change | Call onChange in the event handler |
| Share logic between handlers | Extract a function, call from both handlers |
| Chain state updates | Calculate all state in one event handler |
You DO need an Effect for:
- Subscribing to browser events (online/offline, resize, intersection)
- Connecting to external systems (WebSocket, third-party widgets)
- Fetching data that depends on current props/state (with cleanup)
- Synchronizing with non-React DOM (imperative animations, canvas)
Every Effect that subscribes must return a cleanup function. Data fetching in Effects must use a cleanup flag (let ignore = false) to prevent race conditions. Prefer a data-fetching library or use() with Suspense over raw Effects for fetching.
Custom Hooks
- Custom hooks share stateful logic, not state itself. Each call creates independent state.
- Must start with
usefollowed by a capital letter. Functions that don't call hooks
should NOT start with use.
- Name after the use case, not the lifecycle —
useOnlineStatusnotuseMount. - Extract when: repetitive Effect logic across components, complex state + Effect combos,
or synchronization with external systems.
- Don't extract a single
useStateinto a hook — that's unnecessary abstraction.
Return value conventions:
- Single value: return directly (
return isOnline) - Value + setter pair: return tuple (
return [value, setValue] as const) - Multiple related values: return object (
return { value, onChange, reset })
useSyncExternalStore
For subscribing to external data stores, prefer useSyncExternalStore over manual Effect + state. Provide a subscribe function, a client snapshot getter, and a server snapshot getter for SSR.
State Management
State Placement Decision Tree
1. Does only one component use it? Keep it local with useState. 2. Do siblings need it? Lift to their closest common parent. 3. Is prop drilling painful (5+ levels)? Try composition first — restructure components to pass JSX as children. 4. Still painful after composition? Use Context with use(). 5. Is it server data? Use a data-fetching library — not useState + useEffect.
Local state → Lift state up → Composition → Context → External libraryServer Cache vs UI State
| Category | Examples | Tool |
|---|---|---|
| UI state | Modal open, form input, selected tab | useState, useReducer, Context |
| Server cache | User data, search results, API responses | react-query, SWR, framework loaders |
Never reinvent caching, deduplication, and race condition handling with raw useState.
useState
- Minimal state. If it can be computed from existing props or state, compute it during
render — don't store it.
- Colocate state. Keep state as close to where it's used as possible. Lift up only when
multiple components need it.
- Use updater functions when next state depends on previous state:
setCount(prev => prev + 1) not setCount(count + 1).
- Lazy initialization for expensive initial values — pass a function:
useState(() => createInitialState()) not useState(createInitialState()).
useReducer
Use when state updates are complex — many event handlers modifying the same state, or when next state depends on previous state in non-trivial ways.
| Signal | Tool |
|---|---|
| Single value, simple updates | useState |
| Multiple related values, complex transitions | useReducer |
| Many event handlers doing similar state updates | useReducer |
| Need to test state logic in isolation | useReducer |
Reducer rules:
- Reducers must be pure — same inputs = same output, no side effects.
- Each action describes a single user interaction — dispatch
reset_formnot five
separate set_field actions.
- Actions describe what happened, not what to do —
'added_task'not'set_tasks'. - Always have a
defaultcase that throws to catch typos early. - Use
as constfor action types in TypeScript.
key for Identity Reset
Use key to reset a component's state when the conceptual entity changes: <Profile key={userId} />. This is cleaner than using an Effect to reset state on prop change.
Context
- Use
<Context value={...}>directly — not<Context.Provider value={...}>. - Always wrap context consumption in a custom hook with a null check that throws if
used outside the provider.
- Try props and composition first. Context is not the first solution to prop drilling.
- Keep context close to where it's used — not every context belongs at the app root.
- Split logically — user settings separate from notifications. Don't put all state
in one giant context.
- Different
createContext()calls are independent — they don't override each other. - For complex shared state, combine
useReducerwith context. Split into two contexts
(data + dispatch) so components that only dispatch don't re-render on data changes.
Actions
- Use
useActionStatefor form submissions and data mutations. It manages pending state,
errors, and sequential action queuing automatically.
- When passed to
<form action>, React wraps submission in a transition automatically.
When calling dispatch manually, wrap in startTransition.
- Return error states instead of throwing to prevent skipping queued actions.
- Use
useOptimisticfor instant UI feedback while async Actions complete. The optimistic
state reverts to real value when the Action completes or fails.
- Use a reducer form of
useOptimisticfor complex updates (e.g., adding to a list). useFormStatusreads submission status of the nearest parent<form>— must be
called from a component rendered inside a <form>, not in the same component.
TypeScript
- Every component with props must have a dedicated named type (e.g.,
ButtonProps).
Never define prop types inline in the function signature. Use function Button(props: ButtonProps) or destructure: function Button({ label }: ButtonProps).
- Don't use
React.FC. Use plain function declarations. - Type events explicitly when needed:
(e: React.ChangeEvent<HTMLInputElement>) => void.
- Use
as constfor action types in reducers.
Performance
React Compiler
React Compiler is a build-time tool that automatically applies memo, useMemo, and useCallback equivalents. When using the compiler:
- Do not manually wrap components in
memo, useuseMemo, oruseCallbackin new code. - Leave existing memoization in place — removing it can change compilation output.
- Use manual memoization only as an escape hatch (e.g., stabilizing an Effect dependency).
- The compiler works with plain JavaScript and the Rules of React — no code changes needed.
Optimization Decision Tree
1. Is there a perceptible lag? No — don't optimize. 2. Is the render itself slow? Profile it. Fix the computation. 3. Are components re-rendering unnecessarily? Restructure first (push state down, lift content up). 4. Still slow after restructuring? Apply memo, useMemo, useCallback.
Fix the slow render before you fix the re-render. Restructuring beats memoization.
Manual Memoization (When Compiler Is Unavailable)
memo(Component)— skip re-rendering when props unchanged. Useful when: component
re-renders often with same props, re-rendering is expensive, parent re-renders for unrelated reasons. Useless when: props always differ, component is cheap, or it re-renders from its own state/context anyway.
useMemo(fn, deps)— cache computed values. Only for genuinely expensive work or
preserving references passed to memoized children.
useCallback(fn, deps)— cache function references. Use when passing callbacks to
memoized children, in custom hooks returning functions, or as Effect dependencies.
Server Components
- Server Components render on the server, send only output to client. No client JS.
- Can read databases, filesystems, APIs directly. Can be
asyncfunctions. - Cannot use
useState,useEffect, or any client-side React APIs. - Default (no directive needed). Client Components require
"use client"at file top. - Server Components can render Client Components as children. Client Components cannot
import Server Components directly.
"use server"marks Server Functions (Actions callable from client), not Server
Components.
- Automatic code-splitting: Client Component imports from Server Components are
code-split automatically.
Streaming with Suspense
Start rendering immediately, stream slower parts as they resolve. Create promises in Server Components, pass to Client Components, read with use() inside <Suspense>.
Bundle Optimization
- Avoid barrel file imports —
import { Button } from '@/components/Button'not
from '@/components'.
- Use
lazy(() => import('./Chart'))with<Suspense>for heavy components. - Parallelize independent data fetches with
Promise.all, never sequential awaits.
Error Handling
Use onCaughtError and onUncaughtError root options on createRoot for fine-grained error reporting — caught errors come from Error Boundaries, uncaught from unhandled throws.
Testing
Philosophy
Tests resemble how users interact with the application. Query by what users see (roles, text, labels), not by implementation details (class names, component internals, test IDs).
Setup
Always use screen for queries — never destructure from render(). Set up userEvent.setup() before rendering.
Queries
Use the highest-priority query that works: getByRole > getByLabelText > getByText > getByTestId (last resort). Use getBy for present elements, queryBy for asserting absence, findBy for async appearance. Full query priority and variant tables in ${CLAUDE_SKILL_DIR}/references/testing.md.
User Interactions
Always prefer userEvent over fireEvent — it simulates real user behavior (focus, blur, keyDown/keyPress/keyUp sequence). Full method catalog in ${CLAUDE_SKILL_DIR}/references/testing.md.
Async Patterns
- Use
waitForfor assertions that need to wait for async operations. - One assertion per
waitForcallback — multiple assertions cause slower failure detection. - Never put side-effects in
waitFor— the callback may run multiple times. - Never pass an empty callback to
waitFor. - Prefer
findByoverwaitFor+getBy.
Testing Actions and Forms
Render the component and interact as a user would. For useFormStatus components, ensure the component is rendered inside a <form> with an action prop.
Rules
- Don't wrap in
actunnecessarily —render()andfireEventalready handle it.
If you see act warnings, fix the root cause (state update after test finishes).
- Don't add
roleattributes to native elements —<button>already hasrole="button". - Make inputs accessible with
typeand<label>— this makes them queryable by role. - If you can't query by role, the element is probably not accessible to screen readers.
- Don't call
cleanupmanually — it's automatic. - Use
@testing-library/jest-dommatchers:toBeInTheDocument(),toBeVisible(),
toBeDisabled(), toHaveTextContent(), toHaveAttribute(), toHaveValue().
Application
When writing React code:
- Apply all conventions silently — don't narrate each rule.
- If an existing codebase contradicts a convention, follow the codebase and
flag the divergence once.
- Always prefer function declarations for components.
When reviewing React code:
- Cite the specific violation and show the fix inline.
- Don't lecture or quote the rule — state what's wrong and how to fix it.
Integration
This skill provides React-specific conventions. The coding skill governs workflow; language skills govern JS/TS choices; this skill governs component architecture, hooks, state management, and rendering discipline.
{
"sources": {
"React Docs - Thinking in React": "https://raw.githubusercontent.com/reactjs/react.dev/main/src/content/learn/thinking-in-react.md",
"React Docs - Keeping Components Pure": "https://raw.githubusercontent.com/reactjs/react.dev/main/src/content/learn/keeping-components-pure.md",
"React Docs - Reusing Logic with Custom Hooks": "https://raw.githubusercontent.com/reactjs/react.dev/main/src/content/learn/reusing-logic-with-custom-hooks.md",
"React Docs - You Might Not Need an Effect": "https://raw.githubusercontent.com/reactjs/react.dev/main/src/content/learn/you-might-not-need-an-effect.md",
"React Docs - Passing Data Deeply with Context": "https://raw.githubusercontent.com/reactjs/react.dev/main/src/content/learn/passing-data-deeply-with-context.md",
"React Docs - Extracting State Logic into a Reducer": "https://raw.githubusercontent.com/reactjs/react.dev/main/src/content/learn/extracting-state-logic-into-a-reducer.md",
"React Docs - Server Components": "https://raw.githubusercontent.com/reactjs/react.dev/main/src/content/reference/rsc/server-components.md",
"React Docs - use Hook": "https://raw.githubusercontent.com/reactjs/react.dev/main/src/content/reference/react/use.md",
"React Docs - useActionState": "https://raw.githubusercontent.com/reactjs/react.dev/main/src/content/reference/react/useActionState.md",
"React Docs - useOptimistic": "https://raw.githubusercontent.com/reactjs/react.dev/main/src/content/reference/react/useOptimistic.md",
"React Docs - useFormStatus": "https://raw.githubusercontent.com/reactjs/react.dev/main/src/content/reference/react-dom/hooks/useFormStatus.md",
"React Docs - React Compiler Introduction": "https://raw.githubusercontent.com/reactjs/react.dev/main/src/content/learn/react-compiler/introduction.md",
"React 19 Release Blog Post": "https://raw.githubusercontent.com/reactjs/react.dev/main/src/content/blog/2024/12/05/react-19.md",
"Vercel React Best Practices (Agent Skills)": "https://raw.githubusercontent.com/vercel-labs/agent-skills/main/skills/react-best-practices/AGENTS.md",
"Vercel Composition Patterns (Agent Skills)": "https://raw.githubusercontent.com/vercel-labs/agent-skills/main/skills/composition-patterns/AGENTS.md",
"Kent C Dodds - Common Mistakes with React Testing Library": "https://kentcdodds.com/blog/common-mistakes-with-react-testing-library",
"Kent C Dodds - Application State Management with React": "https://kentcdodds.com/blog/application-state-management-with-react",
"Patterns.dev - React Server Components": "https://www.patterns.dev/react/react-server-components/",
"Patterns.dev - Compound Pattern": "https://www.patterns.dev/react/compound-pattern/"
},
"lastFetched": "2026-02-16T16:43:58.188Z"
}
Component Design
Pure components, composition patterns, refs, document metadata, and JSX conventions.
Thinking in React
Build UI in five steps:
1. Break UI into component hierarchy. Each component does one thing. If it grows, decompose into subcomponents. 2. Build a static version first. Render UI from data with props only — no state, no interactivity. Lots of typing, no thinking. 3. Find minimal state. For each piece of data, ask: does it change over time? Is it passed from a parent? Can it be computed? If all yes → not state. 4. Identify where state lives. Find every component that renders based on the state. Find their closest common parent. Put state there. 5. Add inverse data flow. Pass state-setter callbacks down so children can update parent state through event handlers.
Component Purity
React assumes every component is a pure function.
A pure component:
- Minds its own business. Does not change objects or variables that
existed before it was called.
- Same inputs, same output. Given the same props + state + context,
always returns the same JSX.
// BAD — mutates external variable during render
let guest = 0;
function Cup() {
guest = guest + 1; // Side effect during render!
return <h2>Tea cup for guest #{guest}</h2>;
}
// GOOD — pure, uses props
function Cup({ guest }: { guest: number }) {
return <h2>Tea cup for guest #{guest}</h2>;
}Local Mutation Is Fine
Creating and mutating objects/arrays within the same render is safe:
function TeaGathering() {
const cups = []; // Created during this render
for (let i = 1; i <= 12; i++) {
cups.push(<Cup key={i} guest={i} />);
}
return cups; // Local mutation — perfectly fine
}Where Side Effects Belong
| Side effect type | Where to put it |
|---|---|
| User clicks, form submits | Event handlers |
| Sync with external system (DOM, network) | useEffect (last resort) |
| Data transformation | Compute during render |
| Shared logic between handlers | Extract a function, call from handlers |
Event handlers don't need to be pure — they run outside of rendering.
Ref as a Prop
Function components accept ref directly as a prop. Do not use forwardRef.
// GOOD — ref is a regular prop
function MyInput({ placeholder, ref }: {
placeholder: string;
ref?: React.Ref<HTMLInputElement>;
}) {
return <input placeholder={placeholder} ref={ref} />;
}
// Usage
<MyInput ref={inputRef} />Never use `forwardRef`. It is deprecated. Pass ref as a regular prop.
Ref Cleanup Functions
Ref callbacks can return a cleanup function, called when the element unmounts:
<input
ref={(node) => {
// Setup: element attached to DOM
node.focus();
// Cleanup: element removed from DOM
return () => {
// cleanup logic here
};
}}
/>Avoid implicit returns in ref callbacks — use block body {} not parentheses:
// BAD — implicit return confuses TypeScript (looks like cleanup)
<div ref={current => (instance = current)} />
// GOOD — explicit block body
<div ref={current => { instance = current }} />Document Metadata
Render <title>, <meta>, and <link> tags directly in components. React hoists them to <head> automatically:
function BlogPost({ post }: { post: Post }) {
return (
<article>
<title>{post.title}</title>
<meta name="author" content={post.author} />
<meta name="keywords" content={post.keywords} />
<link rel="author" href={post.authorUrl} />
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}This works with client-only apps, streaming SSR, and Server Components. For complex metadata needs (route-based overrides), a metadata library may still be useful.
Custom Elements
React provides full support for custom elements with proper attribute and property handling:
- Server rendering: primitive props (
string,number,true) render
as attributes. Non-primitive props (object, function, false) are omitted.
- Client rendering: props matching a property on the element instance are
assigned as properties; others are assigned as attributes.
<my-component custom-attr="value" items={complexArray} />Composition Patterns
Children as Props
Reduce prop drilling by passing JSX as children:
// BAD — drilling someState through Layout
<Layout someState={someState} onStateChange={setSomeState} />
// GOOD — compose at the call site
<Layout>
<Sidebar>
<SomeLink someState={someState} />
</Sidebar>
<MainContent>
<SomeComponent someState={someState} />
</MainContent>
</Layout>When a wrapper component updates its own state, React knows its children props haven't changed, so children skip re-rendering.
Compound Components
Group related components that share implicit state:
const FlyOutContext = createContext<{
open: boolean;
toggle: (v: boolean) => void;
} | null>(null);
function FlyOut({ children }: { children: React.ReactNode }) {
const [open, toggle] = useState(false);
return (
<FlyOutContext value={{ open, toggle }}>
{children}
</FlyOutContext>
);
}
FlyOut.Toggle = function Toggle() {
const { open, toggle } = use(FlyOutContext)!;
return <button onClick={() => toggle(!open)}>Menu</button>;
};
FlyOut.List = function List({ children }: { children: React.ReactNode }) {
const { open } = use(FlyOutContext)!;
return open ? <ul>{children}</ul> : null;
};
// Usage
<FlyOut>
<FlyOut.Toggle />
<FlyOut.List>
<li>Edit</li>
<li>Delete</li>
</FlyOut.List>
</FlyOut>Controlled vs Uncontrolled
- Controlled: Parent owns the state, passes value + onChange.
Full control, more wiring.
- Uncontrolled: Component manages its own state internally.
Less wiring, less flexibility.
Prefer controlled components when parent needs to coordinate state across siblings. Prefer uncontrolled for isolated, self-contained UI.
Component Body Organization
Separate logic from rendering. The component body handles computation, state, and handler definitions. JSX is declarative — it references results, not processes.
Named Prop Types
Every component with props must have a dedicated named type. Never define prop types inline in the function signature:
// BAD — inline prop type
function SavePopover({ names, onCreate }: {
names: ReadonlySet<string>;
onCreate: (name: string) => void;
}) { ... }
// GOOD — dedicated named type
interface SavePopoverProps {
readonly names: ReadonlySet<string>;
readonly onCreate: (name: string) => void;
}
function SavePopover({ names, onCreate }: SavePopoverProps) { ... }Handler Object
Group all event handlers in a single handle object. This creates a clear boundary between logic and rendering:
const handle = {
submit() {
if (!canSubmit) return;
onSubmit(value);
reset();
},
inputChange(e: ChangeEvent<HTMLInputElement>) {
setValue(e.target.value);
},
keyDown(e: KeyboardEvent) {
if (e.key === 'Enter') handle.submit();
},
};
return <input onChange={handle.inputChange} onKeyDown={handle.keyDown} />;Never inline handler logic in JSX. Reference handlers from the handle object.
Pre-render Computation
Move list rendering and derived JSX out of the return statement into component body variables:
// BAD — iteration logic inside JSX
return (
<TabList>
{allTabs.map((tab) => (
<Tab key={tab.id}>{tab.name}</Tab>
))}
</TabList>
);
// GOOD — computed before the return
const tabElements: ReactNode[] = [];
for (const tab of allTabs) {
tabElements.push(<Tab key={tab.id}>{tab.name}</Tab>);
}
return <TabList>{tabElements}</TabList>;Conditional Rendering
Simple conditions are acceptable inline in JSX:
{isModified && <SaveButton />}When the condition is complex or involves multiple branches, compute in the body:
// BAD — complex logic in JSX
{items.length > 0 && hasPermission && !isLoading && <ItemList items={items} />}
// GOOD — computed in body
const showItems = items.length > 0 && hasPermission && !isLoading;
// ...
{showItems && <ItemList items={items} />}JSX Conventions
- Self-closing tags for components without children:
<Input />. - Boolean attributes without value:
<Input disabled />not
disabled={true}.
- Fragments to avoid wrapper divs:
<>...</>or<Fragment key={id}>. - Avoid `&&` with numbers.
count && <List />renders0.
Use count > 0 && <List /> or ternary.
- Never inline handler logic in JSX. Group all handlers in a
handle
object in the component body (see Handler Object above).
- Spread props sparingly.
{...props}makes it unclear what a
component accepts. Prefer explicit props.
Hooks
The use() API, custom hooks, Effects, and hook design patterns.
use() — Read Context and Promises
use() reads resources during render. It accepts context or promises.
Reading Context
use() replaces useContext() and is more flexible — it can be called inside conditionals and loops:
import { use } from 'react';
function Button() {
const theme = use(ThemeContext);
return <button className={`btn-${theme}`}>Click</button>;
}
// Works after early returns — useContext cannot do this
function HorizontalRule({ show }: { show: boolean }) {
if (!show) return null;
const theme = use(ThemeContext);
return <hr className={theme} />;
}use() always looks for the closest provider above the calling component. It does not consider providers in the same component.
Reading Promises
use() integrates with Suspense and Error Boundaries to read promise values:
import { use, Suspense } from 'react';
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise);
return comments.map(c => <p key={c.id}>{c.text}</p>);
}
function Page({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
return (
<Suspense fallback={<p>Loading comments...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
);
}Rules for `use()` with promises:
- Do not create promises inside Client Components during render — they
recreate on every render. Pass promises from Server Components or use a Suspense-compatible library.
- In Server Components, prefer
async/awaitoveruse(). - Cannot be called in a try-catch block. Use Error Boundaries or
promise.catch() instead.
Streaming Data from Server to Client
Create the promise in a Server Component and pass it to a Client Component:
// Server Component
async function Page() {
const commentsPromise = fetchComments();
return (
<Suspense fallback={<p>Loading...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
);
}
// Client Component
'use client';
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise);
return comments.map(c => <p key={c.id}>{c.text}</p>);
}This avoids blocking Server Component rendering — the promise streams to the client and resolves there.
Effects Are Escape Hatches
You don't need an Effect for:
| Situation | Instead of Effect | Do this |
|---|---|---|
| Transform data for rendering | useEffect(() => setFiltered(...)) | Compute during render |
| Handle user events | useEffect(() => { if (submitted) post(...) }) | Call in event handler |
| Reset state on prop change | useEffect(() => setComment(''), [userId]) | Use key={userId} |
| Adjust state on prop change | useEffect(() => setSelection(null), [items]) | Compute: items.find(...) |
| Notify parent of state change | useEffect(() => onChange(value), [value]) | Call onChange in handler |
| Share logic between handlers | useEffect with flags | Extract function, call from both handlers |
| Chain state updates | Multiple Effects triggering each other | Calculate all state in one event handler |
You DO need an Effect for:
- Subscribing to browser events (online/offline, resize, intersection)
- Connecting to external systems (WebSocket, third-party widgets)
- Fetching data that depends on current props/state (with cleanup)
- Synchronizing with non-React DOM (imperative animations, canvas)
Data Fetching in Effects
Always add cleanup to prevent race conditions:
useEffect(() => {
let ignore = false;
fetchResults(query).then(json => {
if (!ignore) {
setResults(json);
}
});
return () => { ignore = true; };
}, [query]);Better: use a data-fetching library (react-query, SWR, framework loaders) or use() with Suspense. Raw Effects for fetching are a last resort.
Effect Cleanup
Every Effect that subscribes must return a cleanup function:
useEffect(() => {
const handler = () => setIsOnline(navigator.onLine);
window.addEventListener('online', handler);
window.addEventListener('offline', handler);
return () => {
window.removeEventListener('online', handler);
window.removeEventListener('offline', handler);
};
}, []);Custom Hooks
When to Extract
- Repetitive Effect logic across multiple components.
- Complex state + Effect combinations that obscure component intent.
- Synchronization with external systems — wrap in a hook to
hide implementation details.
You don't need to extract every little piece of duplication. A single useState wrapped in a hook is usually unnecessary.
Naming and Design
Custom hooks share stateful logic, not state itself. Each call to the same hook creates independent state.
Naming rules:
- Must start with
usefollowed by a capital letter. - Functions that don't call hooks should NOT start with
use. - Name after the use case, not the lifecycle:
// BAD — lifecycle wrapper
function useMount(fn: () => void) { useEffect(fn, []); }
function useUpdateEffect(fn: () => void) { /* ... */ }
// GOOD — concrete use case
function useOnlineStatus() { /* ... */ }
function useChatRoom(options: ChatOptions) { /* ... */ }
function useFormInput(initialValue: string) { /* ... */ }
function useIntersectionObserver(ref: RefObject<Element>) { /* ... */ }Return Values
- Single value: return it directly (
return isOnline;). - Value + setter pair: return a tuple (
return [value, setValue] as const;). - Multiple related values: return an object
(return { value, onChange, reset };).
External Store Subscription
For subscribing to external data stores, prefer useSyncExternalStore over manual Effect + state:
function useOnlineStatus() {
return useSyncExternalStore(
subscribe,
() => navigator.onLine, // client
() => true // server (SSR)
);
}
function subscribe(callback: () => void) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}Hook Rules
- Top level only. Never call hooks inside conditions, loops, or
nested functions. React relies on call order. Exception: use() can be called conditionally.
- React functions only. Call hooks from function components or
custom hooks — never from regular JavaScript functions.
- Exhaustive deps. Include all reactive values used inside the
Effect in the dependency array. The linter enforces this — don't suppress it. If a dependency causes unwanted re-runs, restructure the code.
- One Effect per concern. Don't merge unrelated sync logic into
a single Effect. Separate Effects for separate external systems.
Performance
React Compiler, manual memoization, Server Components, and bundle optimization.
React Compiler
React Compiler is a build-time tool that automatically applies the equivalent of memo, useMemo, and useCallback to all components and hooks. When using the compiler, manual memoization is largely unnecessary.
What It Does
- Skips cascading re-renders. When a parent re-renders, only genuinely
affected children re-render — without manual memo() wrapping.
- Memoizes expensive calculations. Function calls inside components
and hooks are automatically cached.
- Handles subtle bugs. The compiler correctly optimizes patterns that
break manual memoization (e.g., inline arrow functions as props).
What to Do About useMemo, useCallback, and memo
- New code: Rely on the compiler. Use
useMemo/useCallbackonly
as an escape hatch when you need precise control (e.g., stabilizing an Effect dependency).
- Existing code: Leave existing memoization in place (removing it can
change compilation output) or test carefully before removing.
- The compiler works with plain JavaScript and the Rules of React — no
code changes needed.
When Compiler Is Not Available
If your project does not use React Compiler, follow the manual memoization strategies below. But prefer enabling the compiler first.
Before You Optimize
Fix the slow render before you fix the re-render. Most performance problems come from slow rendering logic, not unnecessary re-renders.
Optimization Decision Tree
1. Is there a perceptible lag? If no — don't optimize. 2. Is the render itself slow? Profile it. Fix the computation. 3. Are components re-rendering unnecessarily? Try restructuring first. 4. Still slow after restructuring? Apply memo, useMemo, useCallback.
Restructuring beats memoization. Pushing state down or lifting content up often eliminates re-renders without any memoization API.
Manual Memoization (Escape Hatch)
Use these only when React Compiler is not available or when you need precise control over a specific memoization boundary.
memo
Wraps a component to skip re-rendering when props are unchanged:
const ExpensiveList = memo(function ExpensiveList({ items }: { items: Item[] }) {
return <ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
});When memo helps:
- Component re-renders often with the same exact props
- Re-rendering is visibly expensive
- Parent re-renders frequently for unrelated reasons
When memo is useless:
- Props are always different (new object/array/function each render)
- Component is cheap to render
- Component always re-renders anyway (its own state or context changes)
useMemo
Caches a computed value between renders:
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);Only use for genuinely expensive work or for preserving references passed to memoized children.
useCallback
Caches a function reference between renders:
const handleClick = useCallback(() => { doSomething(id); }, [id]);Use when passing callbacks to memoized children, in custom hooks that return functions, or as Effect dependencies.
Server Components
Server Components render on the server and send only the output to the client. The component code and its dependencies are never included in the client bundle.
Key Properties
- No client-side JavaScript. Server Component code stays on the server.
- Direct data access. Can read databases, filesystems, APIs directly.
- Async/await in components. Server Components can be
asyncfunctions. - No hooks, no state, no effects. They cannot use
useState,useEffect,
or any client-side React APIs.
- Automatic code-splitting. Client Component imports from Server
Components are automatically code-split.
Server vs Client Components
| Feature | Server Component | Client Component |
|---|---|---|
| Render environment | Server (build or request time) | Browser |
| Bundle impact | Zero — not shipped | Included in JS bundle |
| Data access | Direct (DB, filesystem, APIs) | Via fetch/API calls |
| Interactivity | None — no state, no events | Full — hooks, events, DOM |
| Directive | Default (no directive needed) | "use client" at top of file |
Composition Pattern
Server Components can render Client Components as children. Client Components cannot import Server Components directly.
// Server Component — fetches data, no client JS
async function NotesPage() {
const notes = await db.notes.getAll();
return (
<div>
{notes.map(note => (
<Expandable key={note.id}>
<p>{note.content}</p>
</Expandable>
))}
</div>
);
}
// Client Component — handles interactivity
"use client";
function Expandable({ children }: { children: ReactNode }) {
const [expanded, setExpanded] = useState(false);
return (
<div>
<button onClick={() => setExpanded(!expanded)}>Toggle</button>
{expanded && children}
</div>
);
}Server Functions (Server Actions)
Server Functions allow Client Components to call async functions executed on the server. Defined with the "use server" directive:
// actions.ts
'use server';
export async function addTodo(text: string) {
await db.todos.create({ text });
}`"use server"` marks Server Functions, not Server Components. Server Components need no directive — they are the default.
Streaming with Suspense
Start rendering immediately, stream slower parts as they resolve:
async function Page({ id }: { id: string }) {
const note = await db.notes.get(id);
const commentsPromise = db.comments.get(note.id);
return (
<div>
<NoteContent note={note} />
<Suspense fallback={<p>Loading comments...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</div>
);
}Bundle Optimization
Avoid Barrel File Imports
// BAD — may pull in entire library
import { Button } from '@/components';
// GOOD — direct import
import { Button } from '@/components/Button';Dynamic Imports for Heavy Components
const Chart = lazy(() => import('./Chart'));
function Dashboard() {
return (
<Suspense fallback={<ChartSkeleton />}>
<Chart data={data} />
</Suspense>
);
}Parallelize Independent Data Fetches
// BAD — sequential waterfall
const user = await getUser(id);
const posts = await getPosts(id);
// GOOD — parallel
const [user, posts] = await Promise.all([getUser(id), getPosts(id)]);Error Handling
Use onCaughtError and onUncaughtError root options for fine-grained error reporting:
const root = createRoot(document.getElementById('root'), {
onCaughtError(error, errorInfo) {
// Errors caught by Error Boundaries
logToService('caught', error, errorInfo);
},
onUncaughtError(error, errorInfo) {
// Errors not caught by any Error Boundary
logToService('uncaught', error, errorInfo);
},
});Anti-Patterns
| Don't | Do |
|---|---|
Wrap every component in memo | Let React Compiler handle it; profile first |
useMemo for cheap computations | Just compute inline |
useCallback on every function | Only when needed as escape hatch |
Fetch in useEffect without cleanup | Use a data library or use() with Suspense |
"use client" on everything | Default to Server Components; add "use client" only for interactivity |
| Sequential awaits for independent data | Promise.all for parallel fetches |
| Barrel file imports | Direct imports to specific modules |
State Management
useState, useReducer, context, Actions, useActionState, useOptimistic, useFormStatus, state placement, and data flow patterns.
State Placement
The most important decision in React state management is where state lives.
Decision Tree
1. Does only one component use it? Keep it local with useState. 2. Do siblings need it? Lift to their closest common parent. 3. Is prop drilling painful (5+ levels)? Try composition first — restructure components to pass JSX as children. 4. Still painful after composition? Use Context with use(). 5. Is it server data (user profile, search results)? Use a data-fetching library (react-query, SWR, framework loaders) — not useState + useEffect.
Local state (useState)
→ Lift state up (shared parent)
→ Composition (children, render props)
→ Context (use())
→ External library (react-query, jotai, zustand)Server Cache vs UI State
These are fundamentally different:
| Category | Examples | Tool |
|---|---|---|
| UI state | Modal open, form input, selected tab | useState, useReducer, Context |
| Server cache | User data, search results, API responses | react-query, SWR, framework loaders |
Server cache has inherently different problems — caching, deduplication, race conditions, background refetching. Don't reinvent these with raw useState.
useState
For simple, independent values:
const [count, setCount] = useState(0);
const [name, setName] = useState('');Updater Functions
When next state depends on previous state, use the updater form:
// BAD — stale closure if called multiple times in one event
setCount(count + 1);
// GOOD — always uses latest state
setCount(prev => prev + 1);Lazy Initialization
For expensive initial values, pass a function:
// BAD — createInitialState() runs every render
const [state, setState] = useState(createInitialState());
// GOOD — runs only on mount
const [state, setState] = useState(() => createInitialState());useReducer
Use when state updates are complex — many event handlers modifying the same state, or when the next state depends on the previous state in non-trivial ways.
When to Choose useReducer over useState
| Signal | Tool |
|---|---|
| Single value, simple updates | useState |
| Multiple related values, complex transitions | useReducer |
| Many event handlers doing similar state updates | useReducer |
| Need to test state logic in isolation | useReducer |
Reducer Pattern
type Action =
| { type: 'added'; id: number; text: string }
| { type: 'changed'; task: Task }
| { type: 'deleted'; id: number };
function tasksReducer(tasks: Task[], action: Action): Task[] {
switch (action.type) {
case 'added':
return [...tasks, { id: action.id, text: action.text, done: false }];
case 'changed':
return tasks.map(t => t.id === action.task.id ? action.task : t);
case 'deleted':
return tasks.filter(t => t.id !== action.id);
default:
throw new Error(`Unknown action: ${(action as Action).type}`);
}
}
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);Reducer Rules
- Reducers must be pure. Same inputs = same output. No side effects.
- Each action describes a single user interaction. Dispatch
reset_form,
not five separate set_field actions.
- Actions describe what happened, not what to do.
'added_task'not
'set_tasks'.
- Always have a `default` that throws — catch typos early.
Context with use()
Provides data to an entire subtree without prop drilling.
Pattern: Context + Provider + Hook
// 1. Create context
const CountContext = createContext<{
count: number;
setCount: Dispatch<SetStateAction<number>>;
} | null>(null);
// 2. Provider component — use <Context> directly, not <Context.Provider>
function CountProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
return (
<CountContext value={{ count, setCount }}>
{children}
</CountContext>
);
}
// 3. Consumer hook with safety check — use() instead of useContext()
function useCount() {
const context = use(CountContext);
if (!context) {
throw new Error('useCount must be used within a CountProvider');
}
return context;
}Context Rules
- Try props and composition first. Context is not the first solution
to prop drilling.
- Keep state close to where it's used. Not every context belongs at
the app root. Page-level or feature-level providers are fine.
- Split logically. User settings context separate from notifications
context. Don't put all state in one giant context.
- Different contexts don't override each other. Each
createContext()
is independent.
Actions
Actions are async functions in transitions that handle pending states, errors, forms, and optimistic updates automatically.
useActionState
Manages state for the result of an Action — like useReducer but with side effects:
const [state, dispatch, isPending] = useActionState(
async (previousState, formData) => {
const error = await updateName(formData.get('name'));
if (error) return error;
redirect('/profile');
return null;
},
null,
);
return (
<form action={dispatch}>
<input type="text" name="name" />
<button type="submit" disabled={isPending}>Update</button>
{state && <p className="error">{state}</p>}
</form>
);Key behaviors:
- When passed to a
<form action>, React wraps the submission in a
transition automatically — no need for startTransition.
- When calling
dispatchmanually (outside a form), wrap instartTransition. - Actions are queued sequentially — each receives the previous result
as previousState.
- Return error states instead of throwing to prevent skipping queued actions.
useOptimistic
Shows instant UI feedback while an async Action completes:
const [optimisticName, setOptimisticName] = useOptimistic(currentName);
async function submitAction(formData: FormData) {
const newName = formData.get('name') as string;
setOptimisticName(newName); // Instant update
const result = await updateName(newName); // Server call
onUpdateName(result); // Commit real value
}
return (
<form action={submitAction}>
<p>Your name is: {optimisticName}</p>
<input type="text" name="name" />
</form>
);Rules:
- The setter must be called inside
startTransitionor an Action prop. - When the Action completes (or fails), the optimistic state reverts to
the real value — no extra render to "clear" it.
- Use a reducer for complex optimistic updates (e.g., adding to a list):
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(currentTodos, newTodo) => [
...currentTodos,
{ ...newTodo, pending: true }
]
);useFormStatus
Reads the submission status of the nearest parent <form> — useful for design system components:
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}Must be called from a component rendered inside a `<form>`. It does not track forms rendered in the same component.
Combining Reducer + Context
For complex state shared across a subtree, combine useReducer with context:
const TasksContext = createContext<Task[] | null>(null);
const TasksDispatchContext = createContext<Dispatch<TaskAction> | null>(null);
function TasksProvider({ children }: { children: ReactNode }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
{children}
</TasksDispatchContext>
</TasksContext>
);
}Split into two contexts so components that only dispatch don't re-render when task data changes.
Resetting State with key
Use key to reset a component's state when the conceptual entity changes:
<Profile key={userId} userId={userId} />This is cleaner than using an Effect to reset state on prop change.
Anti-Patterns
| Don't | Do |
|---|---|
| Store computed values in state | Compute during render |
| Sync state with Effect | Derive values, or compute in event handler |
| Put all state in a global store | Keep state local, lift only when needed |
| One giant context for everything | Multiple focused contexts |
useState + useEffect for server data | Data-fetching library |
| Reset state in Effect on prop change | Use key prop |
| Manual pending/error state for mutations | useActionState |
| Delayed UI during mutations | useOptimistic for instant feedback |
Testing
React Testing Library conventions, query priority, userEvent, and common mistakes.
Philosophy
Tests should resemble how users interact with the application. Query by what users see (roles, text, labels), not by implementation details (class names, component internals, test IDs).
Setup
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';Always use screen for queries — never destructure from render():
// BAD
const { getByRole } = render(<Example />);
const button = getByRole('button');
// GOOD
render(<Example />);
const button = screen.getByRole('button');Query Priority
Use queries in this order. Prefer the highest-priority query that works:
| Priority | Query | When to use |
|---|---|---|
| 1 | getByRole | Almost always — buttons, inputs, headings, links |
| 2 | getByLabelText | Form inputs with associated labels |
| 3 | getByPlaceholderText | When label is absent (not ideal) |
| 4 | getByText | Non-interactive elements (paragraphs, spans) |
| 5 | getByDisplayValue | Current value of filled-in inputs |
| 6 | getByAltText | Images |
| 7 | getByTitle | Rarely — title attribute |
| 8 | getByTestId | Last resort — when nothing else works |
getByRole Is Your Default
getByRole queries by ARIA role and accessible name. It works with implicit roles — no need to add role="button" to <button>:
// <button>Submit</button>
screen.getByRole('button', { name: /submit/i });
// <h1>Welcome</h1>
screen.getByRole('heading', { name: /welcome/i, level: 1 });
// <input type="text" /> with <label>Email</label>
screen.getByRole('textbox', { name: /email/i });
// <a href="/about">About</a>
screen.getByRole('link', { name: /about/i });Query Variants
| Variant | No match | Multiple matches | Use for |
|---|---|---|---|
getBy | Throws | Throws | Element exists right now |
queryBy | Returns null | Throws | Asserting element does NOT exist |
findBy | Throws (after timeout) | Throws | Waiting for element to appear |
Rules
- Use `getBy` by default. It throws a helpful error with the full DOM
if the element isn't found.
- Use `queryBy` ONLY to assert non-existence:
expect(screen.queryByRole('alert')).not.toBeInTheDocument();- Use `findBy` for async elements (after data fetches, transitions):
const alert = await screen.findByRole('alert');- Never use `queryBy` to assert existence —
getBygives better errors.
User Interactions
Always prefer userEvent over fireEvent. It simulates real user behavior (keyDown, keyPress, keyUp, focus, blur) rather than dispatching a single event.
const user = userEvent.setup();
// BAD — fires a single change event
fireEvent.change(input, { target: { value: 'hello' } });
// GOOD — fires keyDown, keyPress, keyUp for each character
await user.type(input, 'hello');
// BAD — fires a single click event
fireEvent.click(button);
// GOOD — fires pointerDown, mouseDown, pointerUp, mouseUp, click
await user.click(button);Common userEvent Methods
const user = userEvent.setup();
await user.click(element); // Click
await user.dblClick(element); // Double click
await user.type(input, 'text'); // Type text character by character
await user.clear(input); // Clear input
await user.selectOptions(select, 'value'); // Select option
await user.tab(); // Tab to next focusable element
await user.keyboard('{Enter}'); // Press specific keyTesting Actions and Forms
When testing components that use useActionState or form Actions, render the component and interact with the form as a user would:
const user = userEvent.setup();
render(<MyForm />);
await user.type(screen.getByRole('textbox', { name: /name/i }), 'Alice');
await user.click(screen.getByRole('button', { name: /submit/i }));
// Assert on the result
await screen.findByText('Submitted successfully');For components using useFormStatus, ensure the component is rendered inside a <form> with an action prop in your test setup.
Async Patterns
waitFor
Use for assertions that need to wait for async operations:
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('Error');
});waitFor Rules
- Put one assertion per `waitFor` callback. Multiple assertions
cause slower failure detection.
- Never put side-effects in `waitFor`. The callback may run multiple
times. Fire events outside, assert inside.
- Prefer `findBy` over `waitFor` + `getBy`:
// BAD
const button = await waitFor(() =>
screen.getByRole('button', { name: /submit/i })
);
// GOOD
const button = await screen.findByRole('button', { name: /submit/i });- Never pass an empty callback to
waitFor. Always wait for a
specific assertion.
Assertions
Use @testing-library/jest-dom for better error messages:
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeDisabled();
expect(element).toHaveTextContent(/hello/i);
expect(element).toHaveAttribute('href', '/about');
expect(element).toHaveClass('active');
expect(element).toHaveFocus();
expect(element).toHaveValue('hello');Don't Wrap in act Unnecessarily
render() and fireEvent are already wrapped in act. Adding another act wrapper does nothing. If you see act warnings, fix the root cause (a state update after the test finishes) instead of wrapping in act.
Accessibility in Tests
- Don't add `role` attributes to native elements.
<button>already
has role="button".
- Make inputs accessible with `type` and `<label>`. This makes them
queryable by role.
- Query by role to enforce accessibility. If you can't query an element
by role, it's probably not accessible to screen reader users either.
Anti-Patterns
| Don't | Do |
|---|---|
const { getByRole } = render(...) | render(...); screen.getByRole(...) |
fireEvent.change(input, ...) | await user.type(input, ...) |
screen.queryByRole('alert') to assert existence | screen.getByRole('alert') |
container.querySelector('.btn') | screen.getByRole('button') |
getByTestId as first choice | getByRole, getByLabelText, getByText |
await waitFor(() => {}) (empty callback) | await waitFor(() => expect(...)) |
Side-effects inside waitFor | Fire events outside, assert inside |
Multiple assertions in one waitFor | One assertion per waitFor |
Wrapping everything in act | Let render/fireEvent handle it |
role="button" on <button> | Native elements have implicit roles |
Manual afterEach(cleanup) | Automatic — don't call cleanup |