Array.sort() mutates the original array
The sort() method mutates the array in place. Calling setItems(items.sort()) will not trigger a re-render because the array reference is unchanged. Create a new sorted array instead: setItems([...items].sort()).
Add items to array state with spread syntax
To add items to array state, use spread syntax to create a new array: setTodos([...todos, {id, text}]). Alternatively, use the functional setState form: setTodos(todos => [...todos, {id, text}]).
Update nested object state by spreading at each level
When updating nested objects, spread at each level that needs modification to create new objects at every level. Example: setUser({...user, settings: {...user.settings, theme: 'dark'}}) ensures React detects the change.
Never mutate array state with push()
Using Array.push() to add items to state will mutate the array in place. Since the array reference remains the same, React will not trigger a re-render. Instead, create a new array using spread syntax: setItems([...items, newValue]).
Invalid: cache manipulation during render
Modifying a global cache object during component render is invalid. Example: const cache = {}; function Component({id}) { if (!cache[id]) { cache[id] = fetchData(id); } return <div>{cache[id]}</div>; }
Why global mutations during render are problematic
Modifying global variables during render breaks React's purity assumption for rendering. This causes components to behave differently in development versus production, breaks Fast Refresh, and prevents optimizations like React Compiler from working correctly.
Invalid: modifying window properties during render
Assigning to window properties during component render is invalid. Example: function Component({userId}) { window.currentUser = userId; return <div>User: {userId}</div>; }
Invalid: mutating global counter in render
Incrementing a global variable during component render is invalid. Example: let renderCount = 0; function Component() { renderCount++; return <div>Count: {renderCount}</div>; }
globals rule validates against global mutations during render
The globals rule from eslint-plugin-react-hooks validates against assignment and mutation of global variables during the render phase. It ensures that side effects must run outside of render, preventing components from breaking React's assumption that rendering is pure.
Invalid: mutating global array during render
Pushing to a global array during component render is invalid. Example: const events = []; function Component({event}) { events.push(event); return <div>Events: {events.length}</div>; }
Valid: use useEffect to synchronize external state
Use useEffect hook to synchronize external state like the DOM with React state. Example: function Component({title}) { useEffect(() => { document.title = title; }, [title]); return <div>Page: {title}</div>; }
Valid: use useContext for global values
Use useContext hook to access global values instead of reading global variables. Example: function Component() { const user = useContext(UserContext); return <div>User: {user.id}</div>; }
Valid: use useState for counters
Use useState hook instead of global variables for managing counters. Example: function Component() { const [clickCount, setClickCount] = useState(0); const handleClick = () => { setClickCount(c => c + 1); }; return <button onClick={handleClick}>Clicked: {clickCount} times</button>; }
incompatible-library: react-hook-form useWatch is the compatible alternative
The useWatch hook from react-hook-form is compatible with memoization. It takes control and name parameters and returns a value that properly updates when the field changes, making it safe to use with useMemo or React Compiler.
incompatible-library rule purpose
The incompatible-library ESLint rule validates against usage of libraries that are incompatible with memoization (manual or automatic). These libraries were designed before React's memoization rules were fully documented and use patterns that aren't supported by React. When detected, the linter flags these APIs, and React Compiler automatically skips over components that use them to avoid breaking the app.
Interior mutability breaks memoization
Interior mutability is when an object or function keeps its own hidden state that changes over time, even though the reference to it stays the same. This breaks memoization because React only checks if you gave it a different object or function reference, not what's inside. When an API exhibits interior mutability, both manual useMemo and React Compiler's automatic memoization will break.
incompatible-library: react-hook-form watch is incompatible
The react-hook-form watch function exhibits interior mutability and is incompatible with memoization. Using watch('field') with useMemo will cause values to never update. The alternative is to use useWatch with control and name parameters instead.
incompatible-library: TanStack Table useReactTable is incompatible
The TanStack Table useReactTable hook returns a table instance that uses interior mutability and is incompatible with memoization.
incompatible-library: MobX observer breaks memoization
MobX patterns like the observer function break memoization assumptions. The linter does not yet detect MobX usage. If your app doesn't work with React Compiler when using MobX, you may need to use the 'use no memo' directive.
Designing React APIs for memoization compatibility
When designing a library API or hook, ensure that calling the API can be safely memoized with useMemo. If it can't be memoized safely, both manual and React Compiler memoizations will break. Design APIs that return immutable state and use explicit update functions instead of interior mutability patterns.
How memoization breaks with incompatible libraries example
When using incompatible libraries with memoization, values appear to freeze and never update. For example, with react-hook-form's watch: const name = useMemo(() => watch('name'), [watch]); will result in the name value never updating even when the field changes, causing the UI to appear frozen.
preserve-manual-memoization valid example: removing unnecessary memoization with React Compiler
Example of correct code: function Component({ items, sortBy }) { const sorted = [...items].sort((a, b) => { return a[sortBy] - b[sortBy]; }); return <List items={sorted} />; } - When using React Compiler, manual memoization is not needed and can be removed.
preserve-manual-memoization rule validates compiler memoization inference
The preserve-manual-memoization ESLint rule validates that existing manual memoization using useMemo, useCallback, and React.memo is preserved by the React Compiler. The compiler will only compile components and hooks if its inference matches or exceeds the existing manual memoization.
preserve-manual-memoization: incomplete dependency lists are invalid
The preserve-manual-memoization rule flags useMemo and useCallback calls with incomplete dependency arrays as invalid. Incomplete dependencies prevent the compiler from understanding the code's data flow and applying further optimizations. All variables used in the memoized callback or computation must be included in the dependency array.
preserve-manual-memoization: compiler preserves all manual memoization
React Compiler preserves existing useMemo, useCallback, and React.memo calls. If you have manually memoized something, the compiler assumes you had a good reason and will not remove it.
Manual memoization can be removed when using React Compiler
When using React Compiler, you can safely remove manual memoization with useMemo and useCallback and let the compiler handle optimization instead. The compiler will automatically optimize the code without explicit memoization.
preserve-manual-memoization invalid example: useMemo with missing filter dependency
Example of incorrect code: function Component({ data, filter }) { const filtered = useMemo(() => data.filter(filter), [data]); return <List items={filtered} />; } - Missing 'filter' in the dependency array.
preserve-manual-memoization invalid example: useCallback with missing value dependency
Example of incorrect code: function Component({ onUpdate, value }) { const handleClick = useCallback(() => { onUpdate(value); }, [onUpdate]); return <button onClick={handleClick}>Update</button>; } - Missing 'value' in the dependency array.
preserve-manual-memoization valid example: useMemo with complete dependencies
Example of correct code: function Component({ data, filter }) { const filtered = useMemo(() => data.filter(filter), [data, filter]); return <List items={filtered} />; } - All dependencies are included in the dependency array.
purity example: stable IDs from initial state is valid
This code is valid because randomUUID() is called only once during state initialization, not on every render:
function Component() {
const [id] = useState(() => crypto.randomUUID());
return <div key={id}>Content</div>;
}
purity rule validates components are pure functions
The purity ESLint rule validates that React components and hooks are pure by checking that they do not call known-impure functions. React components must be pure functions - given the same props, they should always return the same JSX.
impure functions that violate purity rule
The following functions violate the purity rule because they return different values for the same inputs: Math.random(), Date.now(), new Date(), crypto.randomUUID(), and performance.now().
consequences of impure functions in render
When components use functions like Math.random() or Date.now() during render, they produce different output each time, which breaks React's assumptions and causes bugs like hydration mismatches, incorrect memoization, and unpredictable behavior.
purity example: Math.random() in render is invalid
This code is invalid because Math.random() produces different output every render:
function Component() {
const id = Math.random(); // Different every render
return <div key={id}>Content</div>;
}
purity example: Date.now() in render is invalid
This code is invalid because Date.now() changes every render:
function Component() {
const timestamp = Date.now(); // Changes every render
return <div>Created at: {timestamp}</div>;
}
correct pattern for showing current time
To show the current time, initialize the time in useState and update it in a useEffect with an interval. Do not call Date.now() during render:
function Clock() {
const [time, setTime] = useState(() => Date.now());
useEffect(() => {
const interval = setInterval(() => {
setTime(Date.now());
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>Current time: {time}</div>;
}
Performance issue from synchronous setState in effects
Synchronous setState calls in effects trigger immediate re-renders before the browser can paint, causing performance issues and visual jank.
Synchronous setState in effect with derived data
Setting state immediately inside an effect that just copies props or derives values from props forces an extra render pass. Instead, calculate derived values during rendering. When something can be calculated from the existing props or state, don't put it in state.
setState in effects is acceptable with ref values
Calling setState in an effect is acceptable if the value comes from a ref, such as reading the height from getBoundingClientRect(). useLayoutEffect is appropriate for this pattern since it runs after DOM mutations but before the browser paints.
Invalid pattern: synchronous setState copying props
Synchronous setState in effect that copies props is invalid. Example: useEffect(() => { setItems(data); }, [data]) when data comes from props. Instead, initialize state with the prop value or calculate during render.
Invalid pattern: setting loading state synchronously in effect
Setting loading state synchronously in an effect is invalid. Example: useEffect(() => { setLoading(true); fetchData().then(() => setLoading(false)); }). This causes an extra render on mount before the fetch completes.
Invalid pattern: transforming data in effect
Transforming data in an effect and storing the result in state is invalid. Example: useEffect(() => { setProcessed(rawData.map(transform)); }). Instead, derive the transformed value during render.
Invalid pattern: deriving state from props in effect
Deriving state from props in an effect is invalid. Example: useEffect(() => { setSelected(items.find(i => i.id === selectedId)); }). Instead, calculate the derived value during render without storing it in state.
Valid pattern: calculate derived data during render
The correct approach to deriving data from props is to calculate it during rendering. Example: const selected = items.find(i => i.id === selectedId); return <div>{selected?.name}</div>;. This avoids an extra render cycle.
Valid pattern: setState from ref in useLayoutEffect
A valid pattern is to use setState in useLayoutEffect when reading values from refs. Example: useLayoutEffect(() => { const { height } = ref.current.getBoundingClientRect(); setTooltipHeight(height); }, []);. This is acceptable because the value comes from DOM measurement, not props or existing state.
set-state-in-effect rule overview
The set-state-in-effect ESLint rule validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance. Setting state immediately inside an effect forces React to restart the entire render cycle.
Synchronous setState in effects causes double rendering
When you update state synchronously in an effect, React must re-render your component, apply changes to the DOM, and then run effects again. This creates an extra render pass. React has to render twice: once to apply the state update, then again after effects run. This double rendering is wasteful when the same result could be achieved with a single render.
Transform data during render instead of in effects
Transform data at the top level of your component instead of in effects. This code will naturally re-run when props or state change without triggering additional render cycles.
Why hook order matters
React relies on the order in which hooks are called to correctly preserve state between renders. When hooks are called conditionally or in loops, React loses track of which state corresponds to which hook call, leading to bugs like state mismatches and 'Rendered fewer/more hooks than expected' errors.
rules-of-hooks ESLint rule purpose
The rules-of-hooks ESLint rule validates that components and hooks follow the Rules of Hooks, ensuring that hooks are called in the exact same order on every render to preserve state correctly.
Hooks in conditions violation
Calling hooks conditionally in if/else statements, ternary operators, or with &&/|| operators violates the Rules of Hooks because the order of hook calls becomes unreliable.
Hooks in loops violation
Calling hooks inside loops (for, while, do-while) violates the Rules of Hooks because the number and order of hook calls changes based on loop iterations.
Hooks after early returns violation
Calling hooks after early return statements violates the Rules of Hooks because they will not be called on every render.
Hooks in callbacks and event handlers violation
Calling hooks inside callbacks or event handlers violates the Rules of Hooks because these functions are not called during every render.
Hooks in async functions violation
Calling hooks inside async functions violates the Rules of Hooks because async functions execute independently from the render cycle.
Hooks in class methods violation
Calling hooks inside class methods violates the Rules of Hooks because hooks are only for functional components.
Hooks at module level violation
Calling hooks at module level, outside of any component or hook, violates the Rules of Hooks.
use hook special conditional rules
The use hook is different from other React hooks and can be called conditionally and in loops. However, it cannot be wrapped in try/catch and must be called inside a component or hook.
Conditional useEffect pattern fix
Instead of calling useEffect conditionally, call the hook unconditionally at the top level and check the condition inside the effect. For example, move `if (isLoggedIn)` inside the useEffect callback and add isLoggedIn to the dependency array.
Conditional state initialization pattern fix
Instead of calling useState conditionally, always call useState at the top level and conditionally set the initial value using a ternary operator in the argument to useState.