Use when designing, implementing, or reviewing ANY React code, especially before using optimization patterns (useMemo, useCallback, React.memo), state management, or component patterns - agents must consult this skill because React 19 has fundamental changes: React Compiler handles automatic memoization (useMemo/useCallback often unnecessary), many manual optimization patterns are now obsolete or harmful - covers React 19 features (Actions, useOptimistic, useActionState), migration from old patterns, and understanding when manual optimization is actually needed versus when React Compiler handles it automatically
Master React version upgrades, class to hooks migration, concurrent features adoption, React 19 features, and React Compiler optimization.
๐จ MANDATORY: Reference this skill ANY time you are:
Important: You MUST use TodoWrite before starting to track all workflow steps.
useMemo, useCallback, or React.memo (often unnecessary in React 19)WHY: Agents are trained on pre-React 19 patterns and will default to outdated approaches like manual memoization, which React Compiler now handles automatically. You MUST consult this skill to:
Specific Use Cases:
React 17: Event delegation changes, no event pooling, new JSX transform
React 18: Automatic batching, concurrent rendering, new root API (createRoot), Suspense on server
React 19: Removed ReactDOM.render/hydrate, removed propTypes/defaultProps for functions, removed forwardRef (ref is regular prop), React Compiler for automatic optimization, Actions and new hooks, Server Components stable
Use automated codemods for mechanical transformations:
# React 19 all-in-one migration
npx codemod react/19/migration-recipe
# Or individual codemods
npx codemod react/19/replace-reactdom-render
npx codemod react/19/remove-forwardref
npx types-react-codemod preset-19 # TypeScript
See @codemods.md for complete codemod reference and custom codemod examples.
React 19 Features:
See @migration-patterns.md for detailed before/after code examples.
| Pattern | React 18 | React 19 |
|---|---|---|
| Root API | ReactDOM.render() | createRoot().render() |
| Forward Refs | forwardRef() | ref as regular prop |
| Form State | Manual state/error/pending | useActionState() |
| Optimistic UI | Manual state management | useOptimistic() |
| Event Handlers | useCallback() with deps | useEffectEvent() |
| Memoization | Manual useMemo/useCallback | React Compiler (automatic) |
React 19 Specific:
๐จ CRITICAL: Agents default to useState + useEffect for prop-derived state, which is WRONG.
This section covers patterns from React's official "You Might Not Need an Effect" guide that prevent common ESLint violations and performance issues.
ESLint Rule: react-hooks/set-state-in-effect
"Synchronous setState calls trigger re-renders before the browser can paint, causing performance issues and visual jank. React has to render twice: once to apply the state update, then again after effects run."
What the rule prohibits:
Valid exception: โ setState from ref values (DOM measurements)
When props change and you need to reset component state, use the key prop instead of useEffect.
// โ WRONG: Synchronous setState in useEffect
function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
useEffect(() => {
setComment(''); // ESLint: set-state-in-effect violation
}, [userId]);
return <textarea value={comment} onChange={e => setComment(e.target.value)} />;
}
// Usage
<ProfilePage userId={userId} />
Why this is wrong:
// โ
CORRECT: Key prop resets state automatically
function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
// No useEffect needed! Key prop resets state when userId changes
return <textarea value={comment} onChange={e => setComment(e.target.value)} />;
}
// Usage - key tells React this is a different component
<ProfilePage userId={userId} key={userId} />
Why this works:
When you need to derive/transform data from props, calculate directly during render instead of storing in state. React Compiler handles memoization automatically.
// โ WRONG: Synchronous setState in useEffect
function CapabilityDrawer({ capability }) {
const [parameterValues, setParameterValues] = useState({});
useEffect(() => {
// Even though capability came from async fetch,
// THIS setState call is synchronous
const defaults = capability?.parameters
?.filter(p => p.default)
.reduce((acc, p) => ({ ...acc, [p.name]: p.default }), {});
setParameterValues(defaults); // ESLint: set-state-in-effect violation
}, [capability]);
return <ParameterForm values={parameterValues} />;
}
Why this is wrong:
// โ
CORRECT: Calculate directly during render
function CapabilityDrawer({ capability }) {
// Derive defaults during render - no useMemo needed, React Compiler handles it
const defaultParams = capability?.parameters
?.filter(p => p.default)
.reduce((acc, p) => ({ ...acc, [p.name]: p.default }), {}) ?? {};
// Track user edits only
const [userEdits, setUserEdits] = useState<Record<string, string>>({});
// Combine - no useEffect needed!
const parameterValues = { ...defaultParams, ...userEdits };
return <ParameterForm values={parameterValues} onChange={setUserEdits} />;
}
Why this works:
Note: Only add useMemo if profiling shows the calculation takes >100ms. For most derived state operations, direct calculation is sufficient.
Understanding the distinction between async and synchronous setState:
// โ
VALID: setState in ASYNC callback
useEffect(() => {
fetchData().then(data => {
setState(data); // OK - inside async callback
});
}, []);
// โ INVALID: Synchronous setState even if data was fetched async
const { data } = useQuery(...); // Already fetched
useEffect(() => {
setState(data.value); // NOT OK - synchronous setState
}, [data]);
The key difference:
In the invalid example, even though data came from an async operation (useQuery), the setState call itself is synchronous because it runs immediately when the effect executes.
Only these scenarios justify setState in useEffect:
useEffect(() => {
const timer = setTimeout(() => setState(value), 1000);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
const height = elementRef.current?.offsetHeight;
setHeight(height); // OK - reading from DOM
}, []);
useEffect(() => {
const handler = () => setState(newValue);
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
NOT valid:
| Pattern | โ Wrong Approach | โ Correct Approach |
|---|---|---|
| Reset state on prop change | useEffect(() => setState(initial), [prop]) | key={prop} on component |
| Derive from props | useEffect(() => setState(transform(prop)), [prop]) | const val = transform(prop) (direct calculation) |
| Transform data | Store in state, update in effect | Calculate during render |
| User edits + defaults | Single state with effect | Two states: defaults (calculated) + edits (state) |
| Expensive calculation (>100ms) | Recalculate on every render | useMemo(() => expensive(data), [data]) |
React 19 Trilogy (these skills work together):
optimizing-react-performance - Deep dive into React Compiler, virtualization, concurrent features (useTransition, useDeferredValue), profiling. Use when solving performance problems.enforcing-react-19-conventions - PR review checklist with BLOCK/REQUEST CHANGE/VERIFY workflow for React patterns. Use during code reviews to enforce Chariot conventions.Other Related Skills:
frontend-tanstack - TanStack Query patterns and best practicesusing-zustand-state-management - Complete Zustand usage guide