
React Errors Hooks
- 11 installs
- 6 repo stars
- Updated July 8, 2026
- openaec-foundation/react-claude-skill-package
Helps with frontend development tasks.
About
react-errors-hooks is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted development.
- react-errors-hooks
- Frontend Development
- AI-coding skill
React Errors Hooks by the numbers
- 11 all-time installs (skills.sh)
- Ranked #1,658 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/react-claude-skill-package --skill react-errors-hooksAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 11 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/react-claude-skill-package ↗ |
What it does
Helps with frontend development tasks.
Files
react-errors-hooks
Quick Reference: Error Diagnostic Table
| Symptom | Cause | Fix |
|---|---|---|
React has detected a change in the order of Hooks | Conditional hook call or early return before hooks | Move ALL hooks above any conditional logic or returns |
Invalid hook call | Hook called in regular function, class, or nested scope | ONLY call hooks from function components or custom hooks |
Too many re-renders | setState called during render body | Move state update into event handler or useEffect |
| Component re-renders infinitely with useEffect | Missing dependency array or new object/array in deps | Add [] deps, destructure objects to primitives, or use useMemo |
| State shows old value in setTimeout/setInterval | Stale closure captures initial state value | Use functional updater setState(prev => ...) or useRef |
| State shows old value in event listener | Event listener closed over stale state | Use useRef to hold latest value, or re-register listener |
Warning: Can't perform a React state update on unmounted component | Async callback updates state after unmount | Return cleanup function from useEffect with ignore flag |
React Hook useEffect has a missing dependency | eslint-plugin-react-hooks exhaustive-deps violation | Add the dependency, move code into effect, or restructure |
React Hook "useX" is called conditionally | Hook inside if/else, loop, or after early return | Restructure to call hook unconditionally at top level |
React Hook "useX" cannot be called in a class component | Hook used inside a class component | Convert to function component or extract hook logic |
| Effect fires twice in development | StrictMode double-invocation (expected behavior) | Ensure cleanup function properly reverses setup |
useFormStatus always returns pending: false | Hook called in same component as <form> | Move useFormStatus into a child component rendered inside the form |
---
Rules of Hooks Violations
Rule 1: ALWAYS Call Hooks at the Top Level
NEVER call hooks inside:
- Conditions (
if, ternary,&&) - Loops (
for,while,do...while) - Nested functions or callbacks
try/catch/finallyblocks- After conditional
returnstatements - Event handler functions
- Functions passed to
useMemo,useReducer, oruseEffect
Exception: use() (React 19 only) CAN be called inside conditionals and loops.
// WRONG: Hook after conditional return
function Profile({ userId }: { userId: string | null }) {
if (!userId) return <p>No user selected</p>;
const [user, setUser] = useState<User | null>(null); // ERROR
// ...
}
// CORRECT: Hook before any conditional logic
function Profile({ userId }: { userId: string | null }) {
const [user, setUser] = useState<User | null>(null);
if (!userId) return <p>No user selected</p>;
// ...
}Rule 2: ALWAYS Call Hooks from React Functions
NEVER call hooks from regular JavaScript functions.
// WRONG: Hook in a utility function
function getWindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 }); // ERROR
return size;
}
// CORRECT: Prefix with "use" to make it a custom hook
function useWindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
// ... effect to track window size
return size;
}---
Infinite Re-render Loops
Pattern 1: setState in Render Body
// WRONG: Causes "Too many re-renders"
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1); // Triggers re-render during render
return <p>{count}</p>;
}
// CORRECT: Update in event handler
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}Pattern 2: useEffect Without Dependency Array
// WRONG: Runs every render, setState triggers re-render, infinite loop
function DataLoader() {
const [data, setData] = useState<string[]>([]);
useEffect(() => {
fetchData().then(setData); // No deps = runs every render
});
return <List items={data} />;
}
// CORRECT: Specify dependency array
function DataLoader() {
const [data, setData] = useState<string[]>([]);
useEffect(() => {
fetchData().then(setData);
}, []); // Runs once on mount
return <List items={data} />;
}Pattern 3: New Object/Array Reference in Dependencies
// WRONG: New object every render = deps always "change"
function ChatRoom({ roomId }: { roomId: string }) {
const options = { serverUrl: "https://localhost:1234", roomId };
useEffect(() => {
const conn = createConnection(options);
conn.connect();
return () => conn.disconnect();
}, [options]); // New reference every render = infinite loop
}
// CORRECT: Move object creation inside effect
function ChatRoom({ roomId }: { roomId: string }) {
useEffect(() => {
const options = { serverUrl: "https://localhost:1234", roomId };
const conn = createConnection(options);
conn.connect();
return () => conn.disconnect();
}, [roomId]); // Primitive dependency = stable
}---
Stale Closures
Timer Callbacks Reading Old State
// WRONG: interval callback captures initial count (always 0)
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // Always sets to 1
}, 1000);
return () => clearInterval(id);
}, []); // count not in deps, closure captures 0
}
// CORRECT: Use functional updater
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1); // Reads latest state
}, 1000);
return () => clearInterval(id);
}, []);
}Event Listeners with Stale Refs
// WRONG: Handler captures stale state
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
const [message, setMessage] = useState("");
useEffect(() => {
function handleScroll() {
setScrollY(window.scrollY);
setMessage(`Scrolled ${message.length} chars`); // Stale message
}
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []); // message not in deps
}
// CORRECT: Use ref for latest value
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
const [message, setMessage] = useState("");
const messageRef = useRef(message);
messageRef.current = message;
useEffect(() => {
function handleScroll() {
setScrollY(window.scrollY);
setMessage(`Scrolled ${messageRef.current.length} chars`);
}
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
}---
useEffect Cleanup Mistakes
Missing Cleanup: Subscription Leak
// WRONG: No cleanup, listener accumulates on every re-render
useEffect(() => {
window.addEventListener("resize", handleResize);
}, [handleResize]);
// CORRECT: Return cleanup function
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [handleResize]);Missing Cleanup: Async Race Condition
// WRONG: Stale responses overwrite fresh data
useEffect(() => {
fetchUser(userId).then(user => setUser(user));
}, [userId]);
// CORRECT: Ignore flag prevents stale updates
useEffect(() => {
let ignore = false;
fetchUser(userId).then(user => {
if (!ignore) setUser(user);
});
return () => { ignore = true; };
}, [userId]);Missing Cleanup: Timer Leak
// WRONG: Interval never cleared on unmount
useEffect(() => {
setInterval(() => setCount(c => c + 1), 1000);
}, []);
// CORRECT: Store ID and clear on cleanup
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(id);
}, []);---
useEffect Timing
| Hook | Fires | Blocks Paint? | Use Case |
|---|---|---|---|
useInsertionEffect | Before DOM mutations | Yes | CSS-in-JS libraries ONLY |
useLayoutEffect | After DOM mutations, before paint | Yes | DOM measurement (tooltips, popovers) |
useEffect | After paint | No | Data fetching, subscriptions, most side effects |
NEVER use useLayoutEffect unless you need to measure DOM before the browser paints. It blocks rendering and hurts performance.
NEVER use useInsertionEffect unless you are building a CSS-in-JS library.
---
eslint-plugin-react-hooks
exhaustive-deps: When to Fix vs Suppress
ALWAYS fix by default. Suppression is ONLY acceptable in these rare cases:
| Scenario | Action |
|---|---|
| Missing primitive dependency | Add it to the array |
| Missing object/function dependency | Move creation inside effect, or use useMemo/useCallback |
| Intentionally run only on mount | Use [] and verify no reactive values are read |
| External stable reference (e.g., dispatch, ref) | Safe to omit — setState, dispatch, and refs are stable |
| Genuinely need to read latest value without re-triggering | Use useRef to hold the value |
// WRONG: Suppressing a real dependency
useEffect(() => {
fetchData(query); // query IS reactive
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Bug: runs with stale query
// CORRECT: Include the dependency
useEffect(() => {
fetchData(query);
}, [query]);---
"You Might Not Need an Effect"
NEVER use useEffect for these scenarios:
| Scenario | Wrong Approach | Correct Approach |
|---|---|---|
| Data transformation | useEffect + setState | Calculate during render |
| Expensive computation | useEffect + setState | useMemo |
| Reset state on prop change | useEffect + setState | key prop on component |
| Event-driven logic | useEffect watching state | Put logic in event handler |
| Notify parent of changes | useEffect + parent callback | Call parent callback in event handler |
| External store subscription | useEffect + addEventListener | useSyncExternalStore |
| App initialization | useEffect with [] | Module-level code or guarded top-level call |
---
StrictMode Double-Invocation
In development, React StrictMode runs setup + cleanup + setup for every Effect. This is NOT a bug.
If your component breaks under StrictMode, your cleanup function does not properly reverse the setup:
// WRONG: Breaks under StrictMode (duplicate connections)
useEffect(() => {
const conn = createConnection(roomId);
conn.connect(); // Called twice, two connections open
}, [roomId]);
// CORRECT: Cleanup reverses setup
useEffect(() => {
const conn = createConnection(roomId);
conn.connect();
return () => conn.disconnect(); // Second setup works clean
}, [roomId]);---
Common useState Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
useState(expensiveFn()) | Function runs every render | useState(expensiveFn) — pass reference |
obj.x = 5; setObj(obj) | Same reference, no re-render | setObj({...obj, x: 5}) — new object |
arr.push(item); setArr(arr) | Mutation, same reference | setArr([...arr, item]) |
useState(() => myFn) when storing a function | React calls it as initializer | useState(() => () => myFn) — wrap in arrow |
| Reading state right after setState | State updates are async | Use functional updater or useEffect |
---
Reference Links
- references/examples.md -- Hook error examples with complete wrong/correct code pairs
- references/anti-patterns.md -- Comprehensive catalog of hook mistakes organized by category
Official Sources
- https://react.dev/reference/rules/rules-of-hooks
- https://react.dev/reference/react/useEffect
- https://react.dev/reference/react/useState
- https://react.dev/learn/you-might-not-need-an-effect
- https://react.dev/learn/synchronizing-with-effects
- https://react.dev/learn/removing-effect-dependencies
Hook Anti-Patterns — Complete Catalog
Every common hook mistake organized by category.
Each entry: what is wrong, WHY it fails, and what to do instead.
---
Category 1: Rules of Hooks Violations
AP-1.1: Hook Inside Condition
What: Calling useState, useEffect, or any hook inside an if block, ternary, or && expression.
Why it fails: React tracks hooks by call order. Conditional hooks change the call order between renders, corrupting state for all subsequent hooks.
Fix: Move ALL hooks above conditional logic. Put conditional behavior inside the hook's callback or after hooks are declared.
---
AP-1.2: Hook After Early Return
What: Placing hooks after a return statement that may execute before them.
Why it fails: Same as AP-1.1 — when the early return executes, hooks below it are skipped, changing call order.
Fix: Declare ALL hooks at the top of the component, before any return statements.
---
AP-1.3: Hook Inside Loop
What: Calling hooks inside for, while, or do...while loops.
Why it fails: Loop iterations may vary between renders, changing the number and order of hook calls.
Fix: Use a single useState with an object or array to manage dynamic collections.
---
AP-1.4: Hook in Regular Function
What: Calling hooks from a function that is not a component (capitalized name) or custom hook (use prefix).
Why it fails: React cannot associate hook state with a component if the function is not recognized as a React function.
Fix: Rename to a custom hook with use prefix, or move hook logic into the component.
---
AP-1.5: Hook in Event Handler
What: Declaring useState or useEffect inside an onClick, onChange, or other event handler function.
Why it fails: Event handlers are not React render functions. Hooks must run during rendering, not during event handling.
Fix: Declare hooks at the component top level. Use the setter from useState inside event handlers.
---
AP-1.6: Hook in Class Component
What: Calling hooks inside a React class component's methods.
Why it fails: Hooks are fundamentally incompatible with class components. They rely on function component fiber internals.
Fix: Convert to a function component, or extract hook logic into a wrapper function component.
---
AP-1.7: Hook in try/catch/finally
What: Wrapping hook calls in try/catch blocks.
Why it fails: If the try block throws before all hooks run, subsequent hooks are skipped, changing call order.
Fix: Move hooks outside try/catch. Handle errors within effect callbacks or with Error Boundaries.
---
Category 2: useState Anti-Patterns
AP-2.1: Expensive Initializer Called Every Render
What: useState(createExpensiveObject()) — calling the function with parentheses.
Why it fails: The initializer expression runs every render, even though React only uses the result on mount.
Fix: useState(createExpensiveObject) — pass the function reference without calling it.
---
AP-2.2: Direct State Mutation
What: Modifying state objects or arrays directly, then calling setState with the same reference.
Why it fails: Object.is() comparison sees the same reference and skips re-render. Even if React did re-render, the previous render's state is already corrupted.
Fix: ALWAYS create new objects/arrays: setState({...obj, key: newValue}) or setState([...arr, newItem]).
---
AP-2.3: Reading State Immediately After setState
What: Expecting count to reflect the new value right after setCount(count + 1).
Why it fails: State updates are scheduled for the next render. The current variable holds the snapshot from this render.
Fix: Use functional updater setCount(prev => prev + 1) for chained updates, or derive from the value you just set.
---
AP-2.4: Storing Function in useState
What: useState(myFunction) when you want to store a function as state.
Why it fails: React treats any function passed to useState as a lazy initializer and calls it.
Fix: useState(() => myFunction) — wrap in an arrow function to prevent invocation.
---
AP-2.5: Duplicating Props in State
What: Copying a prop into state and using an effect to sync them.
Why it fails: Creates two sources of truth. State lags behind the prop by one render cycle.
Fix: Use the prop directly. If you need transformation, compute it during render or use useMemo.
---
Category 3: useEffect Anti-Patterns
AP-3.1: Missing Dependency Array
What: useEffect(() => { ... }) without the second argument.
Why it fails: Effect runs after every single render. If it calls setState, it triggers another render, creating an infinite loop.
Fix: ALWAYS provide a dependency array. Use [] for mount-only, or [dep1, dep2] for specific triggers.
---
AP-3.2: Object or Array in Dependencies
What: useEffect(() => { ... }, [someObject]) where someObject is created during render.
Why it fails: JavaScript objects have referential identity. A new {} is never === to the previous {}, so the effect runs every render.
Fix: Destructure to primitive values in the dependency array, move object creation inside the effect, or memoize with useMemo.
---
AP-3.3: Function in Dependencies Without useCallback
What: useEffect(() => { fn() }, [fn]) where fn is defined inline in the component.
Why it fails: Functions are recreated every render. New reference every time = effect runs every render.
Fix: Move the function inside the effect, wrap with useCallback, or extract to module scope if it has no reactive dependencies.
---
AP-3.4: Missing Cleanup for Subscriptions
What: Setting up event listeners, WebSocket connections, or intervals without returning a cleanup function.
Why it fails: On re-render with changed deps, a new subscription is created without removing the old one. On unmount, the subscription persists, causing memory leaks and state updates on unmounted components.
Fix: ALWAYS return a cleanup function that reverses the setup: removeEventListener, .close(), clearInterval.
---
AP-3.5: Async Race Condition Without Ignore Flag
What: Fetching data in useEffect without guarding against stale responses.
Why it fails: User navigates away, dependency changes, or component unmounts while fetch is pending. The response arrives and calls setState on stale or unmounted state.
Fix: Use let ignore = false pattern, AbortController, or a data-fetching library that handles cancellation.
---
AP-3.6: Using useEffect for Derived/Computed State
What: useEffect(() => { setFullName(first + ' ' + last); }, [first, last]).
Why it fails: Creates an unnecessary extra render cycle. Component renders with stale derived value, then effect runs, then re-renders with correct value.
Fix: Compute during render: const fullName = first + ' ' + last. Use useMemo if the computation is expensive.
---
AP-3.7: Using useEffect to Reset State on Prop Change
What: useEffect(() => { setComment(''); }, [userId]).
Why it fails: Renders stale state for one frame before the effect clears it. Visible flicker on fast transitions.
Fix: Use key prop: <CommentBox key={userId} />. React creates a fresh component instance with fresh state.
---
AP-3.8: Using useEffect for Event-Driven Logic
What: Watching state in an effect to trigger a notification or side effect that should only happen on user action.
Why it fails: Effect fires whenever the watched value changes for ANY reason (mount, re-render, parent update), not just user action. Notifications appear on page reload.
Fix: Put the logic in the event handler that triggered the state change.
---
AP-3.9: Effect Chains (Cascading Effects)
What: Multiple useEffect calls where each one sets state that triggers the next.
Why it fails: Each state update causes a re-render. Chain of N effects = N unnecessary intermediate renders. Difficult to trace data flow.
Fix: Consolidate all related state updates into a single event handler or single effect. Use useReducer for complex state transitions.
---
AP-3.10: Subscribing to External Store via useEffect
What: Using useEffect with addEventListener to subscribe to browser APIs or external stores.
Why it fails: Susceptible to tearing (reading inconsistent values during concurrent rendering). Requires manual subscription management.
Fix: Use useSyncExternalStore — designed specifically for external store subscriptions with concurrent rendering safety.
---
Category 4: Stale Closure Anti-Patterns
AP-4.1: Timer with Stale State
What: setInterval(() => setCount(count + 1), 1000) inside a useEffect with [] deps.
Why it fails: The closure captures count at mount time (value 0). Every tick sets count to 0 + 1 = 1.
Fix: Use functional updater: setCount(prev => prev + 1). The updater always receives the current value.
---
AP-4.2: Event Listener with Stale Closure
What: Adding a DOM event listener in a useEffect with [] that reads component state.
Why it fails: The listener closure captures state at the time of registration. State changes are invisible to the listener.
Fix: Use useRef to hold the latest value, updated on every render. Read ref.current in the listener.
---
AP-4.3: Debounced/Throttled Function with Stale Closure
What: Creating a debounced function in useMemo or useCallback that reads state via closure.
Why it fails: The memoized function captures the state from its creation render. When it finally executes after the debounce delay, the state may have changed.
Fix: Pass the current value as an argument to the debounced function instead of reading it from closure.
---
Category 5: Performance Hook Anti-Patterns
AP-5.1: Premature useMemo/useCallback
What: Wrapping every value and function in useMemo/useCallback "for performance."
Why it fails: Memoization has overhead (comparison + cache storage). For cheap computations, memoization costs more than recalculation.
Fix: Profile first with React DevTools. Only memoize when: computation is measurably slow, value is passed to memo() child, or value is a dependency of another hook.
---
AP-5.2: useMemo Without Return (Object Literal)
What: useMemo(() => { key: 'value' }, [dep]) — block body without explicit return.
Why it fails: JavaScript interprets { key: 'value' } as a block with a labeled statement, not an object literal. Returns undefined.
Fix: Wrap in parentheses: useMemo(() => ({ key: 'value' }), [dep]).
---
AP-5.3: memo() with Unstable Props from Parent
What: Wrapping a child in memo() while the parent passes new objects/functions as props every render.
Why it fails: memo() compares props with Object.is(). New object/function reference = always re-renders, making memo() useless.
Fix: In the parent: use useMemo for object props, useCallback for function props. Or pass primitives instead of objects.
---
AP-5.4: Custom arePropsEqual Skipping Function Props
What: Writing a custom comparison for memo() that ignores function props to avoid re-renders.
Why it fails: When the parent re-renders with a new closure, the child keeps the old function. The old function captures stale state, causing bugs.
Fix: NEVER ignore function props in comparisons. Use useCallback in the parent to stabilize function references instead.
---
Category 6: useRef Anti-Patterns
AP-6.1: Reading/Writing ref.current During Render
What: return <div>{myRef.current}</div> or myRef.current = computedValue in the render body.
Why it fails: Refs are not tracked by React. Reading during render makes the component impure (result depends on mutation timing). React may read the component output at unexpected times during concurrent rendering.
Fix: Read/write refs ONLY in event handlers, effects, or during initialization (null check pattern).
---
AP-6.2: Using Ref for Values That Should Trigger Re-render
What: Storing a value in a ref and expecting the UI to update when it changes.
Why it fails: Ref mutations do not trigger re-renders. The screen shows stale data.
Fix: Use useState for any value that affects rendered output.
---
Category 7: useContext Anti-Patterns
AP-7.1: Provider Without value Prop
What: <MyContext> or <MyContext.Provider> without passing a value prop.
Why it fails: Consumers receive undefined instead of the default from createContext.
Fix: ALWAYS pass value to providers: <MyContext value={actualValue}>.
---
AP-7.2: New Object in Provider Value Every Render
What: <ThemeContext value={{ theme, setTheme }}> creating a new object inline.
Why it fails: New object reference on every parent render forces ALL context consumers to re-render, regardless of whether theme or setTheme changed.
Fix: Memoize the value: const value = useMemo(() => ({ theme, setTheme }), [theme, setTheme]).
---
Category 8: React 19 Specific Anti-Patterns
AP-8.1: use() in try/catch
What: Calling use(promise) inside a try/catch block.
Why it fails: Throws "Suspense Exception: This is not a real error!" — React uses this exception internally for Suspense flow control.
Fix: Call use() at the component top level. Use Error Boundaries for error handling.
---
AP-8.2: Creating Promise Inside Client Component for use()
What: Creating a new Promise inside a component and passing it to use().
Why it fails: A new Promise is created every render, causing the component to suspend infinitely.
Fix: Create the Promise in a Server Component and pass it down, or cache/memoize it outside the component.
---
AP-8.3: useFormStatus in Form-Rendering Component
What: Calling useFormStatus() in the same component that renders the <form>.
Why it fails: useFormStatus reads the status of a parent <form>. If there is no parent form, it returns { pending: false }.
Fix: Extract the component using useFormStatus into a child component rendered inside the <form>.
---
AP-8.4: useOptimistic Outside Transition
What: Calling setOptimistic() outside of startTransition or a form action.
Why it fails: The optimistic update briefly renders, then immediately reverts because there is no pending Action to maintain it.
Fix: ALWAYS call setOptimistic inside startTransition or from within a form action handler.
Hook Error Examples — Wrong and Correct Patterns
Complete code examples for every hook error category.
All examples use TypeScript/TSX and are verified against react.dev documentation.
---
1. Rules of Hooks Violations
1.1 Conditional Hook Call
// WRONG: Hook called inside condition
function UserProfile({ userId }: { userId: string | null }) {
if (userId) {
const [user, setUser] = useState<User | null>(null); // ERROR: conditional
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return <div>{user?.name}</div>;
}
return <p>Select a user</p>;
}
// CORRECT: All hooks at top level, condition below
function UserProfile({ userId }: { userId: string | null }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
if (!userId) return;
fetchUser(userId).then(setUser);
}, [userId]);
if (!userId) return <p>Select a user</p>;
return <div>{user?.name}</div>;
}1.2 Hook After Early Return
// WRONG: Hook declared after early return
function Dashboard({ isAdmin }: { isAdmin: boolean }) {
if (!isAdmin) return <p>Access denied</p>;
const [stats, setStats] = useState<Stats | null>(null); // ERROR
useEffect(() => {
loadStats().then(setStats);
}, []);
return <StatsPanel stats={stats} />;
}
// CORRECT: Hooks first, then early returns
function Dashboard({ isAdmin }: { isAdmin: boolean }) {
const [stats, setStats] = useState<Stats | null>(null);
useEffect(() => {
if (!isAdmin) return;
loadStats().then(setStats);
}, [isAdmin]);
if (!isAdmin) return <p>Access denied</p>;
return <StatsPanel stats={stats} />;
}1.3 Hook Inside Loop
// WRONG: Hook called inside a loop
function FieldList({ fields }: { fields: string[] }) {
const values: string[] = [];
for (const field of fields) {
const [value, setValue] = useState(""); // ERROR: hook in loop
values.push(value);
}
return <div>{values.join(", ")}</div>;
}
// CORRECT: Single state object for all fields
function FieldList({ fields }: { fields: string[] }) {
const [values, setValues] = useState<Record<string, string>>(() =>
Object.fromEntries(fields.map((f) => [f, ""]))
);
function handleChange(field: string, value: string) {
setValues((prev) => ({ ...prev, [field]: value }));
}
return (
<div>
{fields.map((field) => (
<input
key={field}
value={values[field] ?? ""}
onChange={(e) => handleChange(field, e.target.value)}
/>
))}
</div>
);
}1.4 Hook in Regular Function
// WRONG: Hook in non-component, non-hook function
function createCounter() {
const [count, setCount] = useState(0); // ERROR: not a React function
return { count, increment: () => setCount((c) => c + 1) };
}
// CORRECT: Custom hook (prefix with "use")
function useCounter(initial: number = 0) {
const [count, setCount] = useState(initial);
const increment = useCallback(() => setCount((c) => c + 1), []);
return { count, increment };
}1.5 Hook in Event Handler
// WRONG: Hook called inside event handler
function SearchForm() {
function handleSubmit() {
const [results, setResults] = useState<string[]>([]); // ERROR
fetchResults().then(setResults);
}
return <button onClick={handleSubmit}>Search</button>;
}
// CORRECT: Hook at component top level
function SearchForm() {
const [results, setResults] = useState<string[]>([]);
function handleSubmit() {
fetchResults().then(setResults);
}
return (
<div>
<button onClick={handleSubmit}>Search</button>
<ResultList items={results} />
</div>
);
}1.6 Hook in try/catch
// WRONG: Hook inside try/catch block
function DataViewer({ id }: { id: string }) {
try {
const [data, setData] = useState<Data | null>(null); // ERROR
return <div>{data?.name}</div>;
} catch {
return <p>Error</p>;
}
}
// CORRECT: Hooks outside try/catch, error boundary for errors
function DataViewer({ id }: { id: string }) {
const [data, setData] = useState<Data | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchData(id)
.then(setData)
.catch((err) => setError(err.message));
}, [id]);
if (error) return <p>Error: {error}</p>;
return <div>{data?.name}</div>;
}---
2. Infinite Re-render Loops
2.1 Calling Handler During Render
// WRONG: onClick calls function immediately (note the parentheses)
function Button() {
const [count, setCount] = useState(0);
return <button onClick={setCount(count + 1)}>Click</button>; // Runs during render!
}
// CORRECT: Pass function reference
function Button() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Click</button>;
}2.2 useEffect Creates New Object in Dependencies
// WRONG: filter object recreated every render
function TodoList({ todos }: { todos: Todo[] }) {
const [filter, setFilter] = useState("all");
const filterConfig = { type: filter, active: true }; // New object each render
useEffect(() => {
applyFilter(filterConfig);
}, [filterConfig]); // Always "changes" = infinite loop
return <div>...</div>;
}
// CORRECT: Destructure to primitives in dependency array
function TodoList({ todos }: { todos: Todo[] }) {
const [filter, setFilter] = useState("all");
useEffect(() => {
const filterConfig = { type: filter, active: true };
applyFilter(filterConfig);
}, [filter]); // Primitive = stable comparison
return <div>...</div>;
}2.3 useSyncExternalStore getSnapshot Returns New Object
// WRONG: New object every call = infinite loop
const size = useSyncExternalStore(subscribe, () => ({
width: window.innerWidth,
height: window.innerHeight,
}));
// CORRECT: Cache the snapshot, return same reference when unchanged
let cachedSize = { width: 0, height: 0 };
function getSnapshot() {
const next = { width: window.innerWidth, height: window.innerHeight };
if (next.width === cachedSize.width && next.height === cachedSize.height) {
return cachedSize;
}
cachedSize = next;
return cachedSize;
}
const size = useSyncExternalStore(subscribe, getSnapshot);---
3. Stale Closures
3.1 Debounced Callback with Stale State
// WRONG: Debounced function captures stale searchTerm
function Search() {
const [searchTerm, setSearchTerm] = useState("");
const [results, setResults] = useState<string[]>([]);
const debouncedSearch = useMemo(
() =>
debounce(() => {
fetchResults(searchTerm).then(setResults); // Stale searchTerm
}, 300),
[] // searchTerm not in deps
);
return <input onChange={(e) => { setSearchTerm(e.target.value); debouncedSearch(); }} />;
}
// CORRECT: Pass value as argument, not via closure
function Search() {
const [searchTerm, setSearchTerm] = useState("");
const [results, setResults] = useState<string[]>([]);
const debouncedSearch = useMemo(
() =>
debounce((term: string) => {
fetchResults(term).then(setResults);
}, 300),
[]
);
return (
<input
onChange={(e) => {
setSearchTerm(e.target.value);
debouncedSearch(e.target.value); // Pass current value directly
}}
/>
);
}3.2 Ref to Hold Latest Value for Callbacks
// Pattern: useRef to always access latest state in long-lived callbacks
function useLatest<T>(value: T): React.MutableRefObject<T> {
const ref = useRef(value);
ref.current = value; // Updated synchronously every render
return ref;
}
function ChatRoom({ roomId, onMessage }: { roomId: string; onMessage: (msg: string) => void }) {
const onMessageRef = useLatest(onMessage);
useEffect(() => {
const conn = createConnection(roomId);
conn.on("message", (msg) => {
onMessageRef.current(msg); // Always calls latest onMessage
});
conn.connect();
return () => conn.disconnect();
}, [roomId]); // onMessage NOT in deps, accessed via ref
}---
4. Cleanup Mistakes
4.1 WebSocket Without Cleanup
// WRONG: Connection stays open after unmount or roomId change
function Chat({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<string[]>([]);
useEffect(() => {
const ws = new WebSocket(`wss://chat.example.com/${roomId}`);
ws.onmessage = (event) => {
setMessages((prev) => [...prev, event.data]);
};
// No cleanup!
}, [roomId]);
}
// CORRECT: Close connection on cleanup
function Chat({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<string[]>([]);
useEffect(() => {
const ws = new WebSocket(`wss://chat.example.com/${roomId}`);
ws.onmessage = (event) => {
setMessages((prev) => [...prev, event.data]);
};
return () => ws.close();
}, [roomId]);
}4.2 AbortController for Fetch Cleanup
// CORRECT: Cancel in-flight requests on cleanup
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then((res) => res.json())
.then((data) => setUser(data))
.catch((err) => {
if (err.name !== "AbortError") {
console.error("Fetch failed:", err);
}
});
return () => controller.abort();
}, [userId]);
return user ? <div>{user.name}</div> : <p>Loading...</p>;
}4.3 IntersectionObserver Without Cleanup
// WRONG: Observer never disconnected
function LazyImage({ src }: { src: string }) {
const imgRef = useRef<HTMLImageElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) setIsVisible(true);
});
if (imgRef.current) observer.observe(imgRef.current);
// No cleanup!
}, []);
}
// CORRECT: Disconnect observer on cleanup
function LazyImage({ src }: { src: string }) {
const imgRef = useRef<HTMLImageElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const node = imgRef.current;
if (!node) return;
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) setIsVisible(true);
});
observer.observe(node);
return () => observer.disconnect();
}, []);
}---
5. eslint-plugin-react-hooks Fixes
5.1 Function Dependency Creates Infinite Loop
// WRONG: createOptions recreated every render, effect runs infinitely
function ChatRoom({ roomId }: { roomId: string }) {
function createOptions() {
return { serverUrl: "https://localhost:1234", roomId };
}
useEffect(() => {
const conn = createConnection(createOptions());
conn.connect();
return () => conn.disconnect();
}, [createOptions]); // New function reference every render
}
// CORRECT: Move function inside effect
function ChatRoom({ roomId }: { roomId: string }) {
useEffect(() => {
const options = { serverUrl: "https://localhost:1234", roomId };
const conn = createConnection(options);
conn.connect();
return () => conn.disconnect();
}, [roomId]); // Only primitive dependency
}5.2 Props Object in Dependencies
// WRONG: Entire props object as dependency
function DataGrid({ config }: { config: GridConfig }) {
useEffect(() => {
initGrid(config);
}, [config]); // Parent re-render = new config object = effect re-runs
return <div id="grid" />;
}
// CORRECT: Destructure to specific properties
function DataGrid({ config }: { config: GridConfig }) {
const { columns, pageSize, sortBy } = config;
useEffect(() => {
initGrid({ columns, pageSize, sortBy });
}, [columns, pageSize, sortBy]); // Primitives are stable
}---
6. "You Might Not Need an Effect" Fixes
6.1 Derived State — Calculate During Render
// WRONG: Extra render cycle for computed value
function FilteredList({ items, query }: { items: Item[]; query: string }) {
const [filtered, setFiltered] = useState<Item[]>([]);
useEffect(() => {
setFiltered(items.filter((item) => item.name.includes(query)));
}, [items, query]);
return <ul>{filtered.map((item) => <li key={item.id}>{item.name}</li>)}</ul>;
}
// CORRECT: Compute during render (or useMemo if expensive)
function FilteredList({ items, query }: { items: Item[]; query: string }) {
const filtered = useMemo(
() => items.filter((item) => item.name.includes(query)),
[items, query]
);
return <ul>{filtered.map((item) => <li key={item.id}>{item.name}</li>)}</ul>;
}6.2 Reset State — Use Key Prop
// WRONG: Effect to reset comment on profile change
function ProfilePage({ userId }: { userId: string }) {
const [comment, setComment] = useState("");
useEffect(() => {
setComment(""); // Flickers: renders old comment first
}, [userId]);
return <textarea value={comment} onChange={(e) => setComment(e.target.value)} />;
}
// CORRECT: Key forces fresh component instance
function ProfilePage({ userId }: { userId: string }) {
return <CommentBox key={userId} />;
}
function CommentBox() {
const [comment, setComment] = useState(""); // Fresh state per key
return <textarea value={comment} onChange={(e) => setComment(e.target.value)} />;
}6.3 Notify Parent — Do It in the Event Handler
// WRONG: Effect to notify parent of state change
function Toggle({ onChange }: { onChange: (isOn: boolean) => void }) {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
onChange(isOn); // Fires on mount too, not just user action
}, [isOn, onChange]);
return <button onClick={() => setIsOn(!isOn)}>{isOn ? "ON" : "OFF"}</button>;
}
// CORRECT: Notify in the same event handler
function Toggle({ onChange }: { onChange: (isOn: boolean) => void }) {
const [isOn, setIsOn] = useState(false);
function handleClick() {
const nextIsOn = !isOn;
setIsOn(nextIsOn);
onChange(nextIsOn); // Batched with setState in same event
}
return <button onClick={handleClick}>{isOn ? "ON" : "OFF"}</button>;
}---
7. React 19 Specific Hook Errors
7.1 use() Outside Component or in try/catch
// WRONG: "Suspense Exception: This is not a real error!"
function DataView({ dataPromise }: { dataPromise: Promise<Data> }) {
try {
const data = use(dataPromise); // ERROR: use() inside try/catch
return <div>{data.name}</div>;
} catch {
return <p>Error</p>;
}
}
// CORRECT: use() at component top level, Error Boundary for errors
function DataView({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise); // Suspends until resolved
return <div>{data.name}</div>;
}
// Wrap in Suspense + ErrorBoundary
<ErrorBoundary fallback={<p>Error</p>}>
<Suspense fallback={<p>Loading...</p>}>
<DataView dataPromise={dataPromise} />
</Suspense>
</ErrorBoundary>7.2 useFormStatus in Wrong Component
// WRONG: useFormStatus in the same component as <form>
function ContactForm() {
const { pending } = useFormStatus(); // Always returns pending: false
return (
<form action={submitContact}>
<input name="email" />
<button disabled={pending}>Send</button>
</form>
);
}
// CORRECT: Extract button into child component
function SubmitButton() {
const { pending } = useFormStatus(); // Reads parent <form> status
return <button disabled={pending}>{pending ? "Sending..." : "Send"}</button>;
}
function ContactForm() {
return (
<form action={submitContact}>
<input name="email" />
<SubmitButton />
</form>
);
}