React best practices
react-best-practices
React and Next.js best practices guide. Use this skill when writing, reviewing, optimizing, or refactoring React components or Next.js code. Covers performance issues, stale closures, unnecessary re-renders, Server Components (RSC), data fetching, Suspense streaming, useTransition/useDeferredValu...
SKILL.md
Full skill instructions
React best practices
A guide to writing correct, performant React — built from real-world patterns and production bugs. Covers 12 categories of rules, from closure gotchas to Server Component architecture.
Instructions
When triggered to write, review, debug, or refactor React or Next.js code, follow this workflow:
- Figure out what the user needs: Are they building something new, auditing existing code, or fixing a specific problem (input lag, stale state, hydration mismatch)?
- Check the rules: Look up the relevant patterns in
AGENTS.mdor the specificrules/*.mdfiles. Don't rely on general knowledge for optimization advice — the rules here are more specific. - Scan for common failure modes (when reviewing or debugging):
- Stale closures: Empty dependency arrays in
useCallbackoruseEffectthat freeze old state values. - Reconciliation traps: Missing keys, array-index keys on reorderable lists, or component definitions inside render functions.
- Waterfall fetching: Sequential
awaitcalls in Server Components that should run in parallel or behind Suspense boundaries. - Main thread blocking: Heavy synchronous state updates that lock up the UI (fix with
startTransition).
- Stale closures: Empty dependency arrays in
- Write the code: Follow the prescribed patterns. Use the right hooks, types, and component structures.
- Explain what you did: After the code block, add an "Architectural notes" section listing which rules you applied and why (e.g., "Applied
closure-stale-callback— the originaluseCallbackhad an empty dep array, freezing the query state").
When to apply
Reference these rules when:
- Writing new React components, Next.js pages, or Server Actions
- Debugging incorrect state, stale closures, or hydration mismatches
- Reviewing code for excessive rendering or UI input lag
- Adding Suspense boundaries, streaming, or concurrent features
- Choosing between
useTransition,useDeferredValue, andReact.memo - Splitting Contexts or passing components as props (slots)
Rule categories by priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Closures & Stale State | CRITICAL | closure- |
| 2 | Reconciliation & Keys | CRITICAL | recon- |
| 3 | Server Components & RSC | CRITICAL | server- |
| 4 | Re-render Causes | HIGH | rerender- |
| 5 | Composition & Context | HIGH | compose- / context- |
| 6 | Suspense & Streaming | HIGH | async- / suspense- |
| 7 | Concurrent Features | MEDIUM | rerender- |
| 8 | Memoization Usage | MEDIUM | memo- |
| 9 | Refs & Imperative APIs | MEDIUM | ref- |
| 10 | DOM Sync & Effects | MEDIUM | effect- |
| 11 | Bundle Optimization | HIGH | bundle- |
| 12 | Rendering Performance | MEDIUM | rendering- |
Quick reference
1. Closures & Stale State (CRITICAL)
closure-understand— Every function closes over its scope at creation timeclosure-stale-callback— Cached functions (useCallback/useRef) freeze state valuesclosure-ref-trick— Use a ref +useLayoutEffectto keep a stable callback that reads fresh state
2. Reconciliation & Keys (CRITICAL)
recon-type-position— React reuses elements with the same type at the same position in the treerecon-key-identity— Stable, unique keys control when React destroys and recreates instancesrecon-no-inline-definition— Defining components inside other components causes unmount loopsrecon-key-reset— Changing a component's key resets its state completely
3. Server Components & Actions (CRITICAL)
server-components— Async RSCs fetch data without adding to the client bundleserver-serialization— Keep data passed from server to client components small and serializableserver-auth-actions— Always authenticate and authorize Server Actions on the server side
4. Re-render Causes (HIGH)
rerender-state-change— State changes cascade re-renders down the component treererender-parent— Parent re-renders force all children to re-render by defaultrerender-context— Context consumers re-render on any value identity changererender-props-myth— Props changes alone don't trigger renders; parent re-renders do
5. Composition & Context Splitting (HIGH)
compose-move-state-down— Isolate changing state in the smallest possible componentcompose-children-prop—childrenaren't re-created when the parent's own state changescompose-components-as-props— Named element props (slots) decouple layout from contentcontext-splitting— Split one big Context into separate ones by domain to reduce render scope
6. Suspense & Streaming (HIGH)
async-suspense-boundaries— Use Suspense to stream independent UI sectionssuspense-parallel-fetching— Run queries in parallel, not sequentially
7. Concurrent Features (MEDIUM)
rerender-transitions—useTransitionkeeps user input responsive during heavy rendersrerender-use-deferred-value—useDeferredValuedefers a prop so the old UI stays visible while the new one computes
8. Memoization Usage (MEDIUM)
memo-react-memo— Wraps a component to skip re-renders when props haven't changedmemo-referential-equality— New object/function references breakReact.memo— stabilize themmemo-usecallback-useless—useCallbackon native DOM elements or un-memoized children wastes cyclesmemo-composition-trap— DynamicchildrenbreakReact.memobecause the JSX object changes every render
9. Refs & Imperative APIs (MEDIUM)
ref-dom-access—useReffor reading DOM measurements and calling imperative APIsref-forward—forwardRef(or the ref prop in React 19) exposes a child's DOM node to its parentref-imperative-handle—useImperativeHandlelimits what the parent can do with the forwarded refref-no-overuse— Refs are an escape hatch. Try declarative patterns first.
10. DOM Sync & Effects (MEDIUM)
effect-layout-flicker—useEffectruns after paint, causing visible flicker;useLayoutEffectruns before painteffect-ssr-ready—useLayoutEffectdoesn't run on the server. Gate it with anisReadystate.
11. Bundle Optimization (HIGH)
bundle-lazy-loading—React.lazy()splits heavy components into separate chunks loaded on demandbundle-tree-shaking-barrel— Barrel files (index.jsre-exports) defeat tree-shaking; use direct imports
12. Rendering Performance (MEDIUM)
rendering-virtualization— Virtualize long lists withreact-windowto avoid rendering thousands of DOM nodesrendering-fragments— Use<>...</>instead of wrapper<div>s to avoid extra DOM depth
Examples
Fixing laggy search input
User says: "My search input feels laggy when typing, and the list takes a long time to filter."
What the agent should do:
- Recognize this as a main-thread-blocking problem —
setStatetriggers a heavy synchronous render. - Apply
rerender-transitions: wrap the expensive state update instartTransition.
Result:
import { useState, useTransition } from 'react';
import { heavyFilter } from './utils';
function SearchPage({ data }) {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
// Urgent: typing updates immediately
setQuery(e.target.value);
// Non-urgent: filtering happens in the background
startTransition(() => {
setResults(heavyFilter(data, e.target.value));
});
};
return (
<div>
<input type="text" value={query} onChange={handleChange} />
{isPending && <span>Updating...</span>}
<ExpensiveList items={results} />
</div>
);
}
Architectural notes:
- Applied
rerender-transitions—startTransitionmoves the heavy filter to a background priority so keystrokes aren't blocked.
Sub-rule files
Individual rule files live in rules/ with full explanations and code examples:
rules/closure-stale-callback.md
rules/server-components.md
rules/concurrent-features.md
Each file has:
- Why it matters — what problem this solves
- Wrong — the buggy or slow pattern
- Right — the fix, with code
- Related rules — other rules that connect to this one
All rules in one file
AGENTS.md has every rule compiled into a single document for loading into an agent's context window.
