React is complaining that your component rendered fewer hooks than it did on a previous render. This usually means you’ve conditionally rendered hooks, which is a big no-no in React’s world.
The core issue is that React keeps a stable list of hooks for each component instance across renders. When the number of hooks changes, or their order is altered, React gets confused because it can’t reliably associate the current hook state with the correct component instance. It’s like trying to find your favorite book on a shelf that keeps rearranging itself.
Here are the most common culprits and how to fix them:
Conditional Hook Rendering
This is the most frequent offender. You’re likely wrapping a hook call inside an if statement, a ternary operator, or any other conditional logic that might prevent the hook from being called on a specific render.
Diagnosis: Carefully examine your component’s render logic. Look for any hook calls (useState, useEffect, useCallback, useMemo, useRef, etc.) that are not always executed on every single render.
Fix: Move all hook calls to the top level of your component, outside of any conditional logic. If you need to conditionally execute the logic within a hook, do that.
// BAD: Hook inside a conditional
function MyComponent({ showDetails }) {
if (showDetails) {
const [data, setData] = useState(null); // THIS IS THE PROBLEM
useEffect(() => {
// ... fetch data
}, []);
// ... render details
}
// ... render other stuff
return <div>...</div>;
}
// GOOD: Hook at top level, logic inside conditional
function MyComponent({ showDetails }) {
const [data, setData] = useState(null); // Hook is always called
useEffect(() => {
if (showDetails) {
// ... fetch data only if showDetails is true
fetch('/api/data')
.then(res => res.json())
.then(setData);
}
}, [showDetails]); // Dependency array is important!
return (
<div>
{showDetails && data && (
<div>
<h2>Details</h2>
<p>{data.description}</p>
</div>
)}
</div>
);
}
Why it works: React guarantees that hooks are called in the exact same order on every render. By moving the hook call to the top level, you ensure it’s always present in React’s internal list, even if its internal logic is conditionally skipped.
Early Returns
Similar to conditional rendering, an early return statement can cause a hook to be skipped.
Diagnosis: Search for return statements that appear before hook calls within your component’s render function.
Fix: Ensure all hook calls are placed before any return statements.
// BAD: Early return before hook
function MyComponent({ userId }) {
if (!userId) {
return <p>Please log in.</p>; // Early return skips the hook
}
const [user, setUser] = useState(null); // Problematic
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
// ...
}
// GOOD: Hook before early return
function MyComponent({ userId }) {
const [user, setUser] = useState(null); // Hook is always called
useEffect(() => {
if (userId) { // Conditional logic *inside* useEffect
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}
}, [userId]);
if (!userId) {
return <p>Please log in.</p>;
}
// ... render user details using 'user' state
return <div>User: {user ? user.name : 'Loading...'}</div>;
}
Why it works: By placing hooks before any potential exit points in the render function, you guarantee they are invoked by React.
Hoisted Hooks (Less Common)
Sometimes, if you’re doing complex logic or using higher-order components (HOCs) in a way that might reorder or conditionally include hooks, you can run into this.
Diagnosis: If you’re using custom hooks or HOCs, inspect their implementation to ensure they consistently apply hooks in the same order. A common pattern is to have a wrapper component or function that always calls its inner hooks.
Fix: Refactor custom hooks or HOCs to ensure all hooks are called unconditionally at the top level of the hook or component they define.
// BAD: Custom hook with conditional hook
function useConditionalFetch(url, shouldFetch) {
const [data, setData] = useState(null);
if (shouldFetch) { // Problematic
useEffect(() => {
fetch(url).then(res => res.json()).then(setData);
}, [url]);
}
return data;
}
// GOOD: Custom hook with conditional logic inside useEffect
function useConditionalFetch(url, shouldFetch) {
const [data, setData] = useState(null);
useEffect(() => {
if (shouldFetch) {
fetch(url).then(res => res.json()).then(setData);
}
}, [url, shouldFetch]); // Depend on shouldFetch too
return data;
}
Why it works: The custom hook itself must adhere to the rules of hooks. By moving the conditional logic inside the useEffect (or other hook), the hook call itself is always present.
State Updates Causing Re-renders with Different Hook Counts
This is a more subtle one. If you’re calling setState in a way that triggers a re-render, but that re-render then leads to a different set of hooks being called (e.g., due to a state change that affects conditional rendering of hooks), you’ll hit this.
Diagnosis: Trace the state updates that precede the error. Is there a state variable that, when changed, alters which hooks are present in the component’s render path?
Fix: The fix is the same as for conditional hook rendering: ensure all hooks are at the top level and use conditional logic within hooks or for rendering their output.
Incorrectly Using useRef for State-like Behavior
While useRef doesn’t directly cause this error, it can be misused in conjunction with state that does cause it. If you’re trying to "remember" something across renders and conditionally render based on that ref, it can indirectly lead to hook mismatches.
Diagnosis: Review your usage of useRef. If a ref’s value is influencing whether other hooks are called, that’s a sign of a potential issue.
Fix: If you need to store state that affects rendering, use useState. If you need to store a mutable value that doesn’t trigger re-renders, useRef is fine, but ensure it doesn’t control the presence of other hooks.
Context API and Hooks
If you’re consuming context and conditionally rendering based on the context value, and that conditional rendering affects hooks, you can see this error.
Diagnosis: Check if your component consumes context and then conditionally renders hooks based on the context’s value.
Fix: Again, the solution is to ensure all hooks are at the top level of your component. The context consumption itself should be done at the top level, and any conditional logic based on the context value should be applied to the rendered output or the behavior within hooks.
If you’ve gone through all these and are still seeing the error, it’s likely a very specific edge case related to how you’re structuring your component or custom hooks. Double-check that no part of your component’s render tree, no matter how deep or indirectly, is causing a hook to be skipped on one render and present on another.
The next error you’ll likely encounter after fixing this is a "Maximum update depth exceeded" error if your state updates are now in an infinite loop, or perhaps a more specific error related to the functionality you were trying to conditionally render.