| name | react-errors-hooks |
| description | Use when encountering hook-related errors, infinite re-render loops, stale state issues, or eslint-plugin-react-hooks warnings. Prevents the common mistake of calling hooks conditionally or missing cleanup in useEffect. Covers Rules of Hooks violations, conditional hook calls, missing dependencies, stale closures, infinite loops, cleanup patterns. Keywords: Rules of Hooks, stale closure, infinite loop, useEffect deps, eslint, too many re-renders, Maximum update depth, exhaustive-deps warning, hook error, infinite loop fix..
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires React 18.x or 19.x with TypeScript. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
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/finally blocks
- After conditional
return statements
- Event handler functions
- Functions passed to
useMemo, useReducer, or useEffect
Exception: use() (React 19 only) CAN be called inside conditionals and loops.
function Profile({ userId }: { userId: string | null }) {
if (!userId) return <p>No user selected</p>;
const [user, setUser] = useState<User | null>(null);
}
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.
function getWindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
return size;
}
function useWindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
return size;
}
Infinite Re-render Loops
Pattern 1: setState in Render Body
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1);
return <p>{count}</p>;
}
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Pattern 2: useEffect Without Dependency Array
function DataLoader() {
const [data, setData] = useState<string[]>([]);
useEffect(() => {
fetchData().then(setData);
});
return <List items={data} />;
}
function DataLoader() {
const [data, setData] = useState<string[]>([]);
useEffect(() => {
fetchData().then(setData);
}, []);
return <List items={data} />;
}
Pattern 3: New Object/Array Reference in Dependencies
function ChatRoom({ roomId }: { roomId: string }) {
const options = { serverUrl: "https://localhost:1234", roomId };
useEffect(() => {
const conn = createConnection(options);
conn.connect();
return () => conn.disconnect();
}, [options]);
}
function ChatRoom({ roomId }: { roomId: string }) {
useEffect(() => {
const options = { serverUrl: "https://localhost:1234", roomId };
const conn = createConnection(options);
conn.connect();
return () => conn.disconnect();
}, [roomId]);
}
Stale Closures
Timer Callbacks Reading Old State
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
}
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(id);
}, []);
}
Event Listeners with Stale Refs
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
const [message, setMessage] = useState("");
useEffect(() => {
function handleScroll() {
setScrollY(window.scrollY);
setMessage(`Scrolled ${message.length} chars`);
}
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
}
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
const [message, setMessage] = useState("");
const messageRef = useRef(message);
messageRef.current = message;
useEffect(() => {
function handleScroll() {
(.);
();
}
.(, handleScroll);
.(, handleScroll);
}, []);
}
useEffect Cleanup Mistakes
Missing Cleanup: Subscription Leak
useEffect(() => {
window.addEventListener("resize", handleResize);
}, [handleResize]);
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [handleResize]);
Missing Cleanup: Async Race Condition
useEffect(() => {
fetchUser(userId).then(user => setUser(user));
}, [userId]);
useEffect(() => {
let ignore = false;
fetchUser(userId).then(user => {
if (!ignore) setUser(user);
});
return () => { ignore = true; };
}, [userId]);
Missing Cleanup: Timer Leak
useEffect(() => {
setInterval(() => setCount(c => c + 1), 1000);
}, []);
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 |
useEffect(() => {
fetchData(query);
}, []);
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:
useEffect(() => {
const conn = createConnection(roomId);
conn.connect();
}, [roomId]);
useEffect(() => {
const conn = createConnection(roomId);
conn.connect();
return () => conn.disconnect();
}, [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
Official Sources