React Hooks Mistakes Developers Make and How to Avoid Them
Learn the most common React Hooks mistakes involving useEffect, useState, dependency arrays, stale values, cleanup, and unnecessary effects—with practical examples and fixes

AI models used: None
Tools used: React DevTools
Prerequisites: JavaScript fundamentals and basic React knowledge
React Hooks Mistakes Developers Make and How to Avoid Them
React Hooks make it possible to use state, effects, memoization, and other React features inside function components.
But Hooks can also introduce confusing bugs when they are used without understanding how rendering and effects work.
A common example is adding useEffect whenever something needs to happen after a state change—even when an effect is not actually necessary.
This guide covers practical React Hooks mistakes, why they happen, and how to write cleaner alternatives.
React Hooks Mistakes at a Glance
| Mistake | Better Approach |
|---|---|
Using useEffect for derived values | Calculate the value during rendering |
| Missing effect dependencies | Declare the values used by the effect |
| Using effects for event handling | Perform the action in the event handler |
| Forgetting cleanup | Clean up subscriptions, timers, and listeners |
| Updating state from stale values | Use functional state updates when appropriate |
| Calling Hooks conditionally | Keep Hooks at the top level |
| Using too many effects | Simplify the component's data flow |
| Ignoring unnecessary state | Derive values when possible |
| Creating infinite effect loops | Check dependencies and state updates |
| Optimizing before measuring | Profile the actual problem |
1. Using useEffect for Derived Data
One of the most common mistakes is storing something in state when it can be calculated directly.
For example:
function ProductList({ products }) {
const [activeProducts, setActiveProducts] = useState([]);
useEffect(() => {
setActiveProducts(
products.filter(product => product.active)
);
}, [products]);
return (
<ul>
{activeProducts.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}Here, activeProducts is derived entirely from products.
You do not necessarily need another state variable or an effect.
A simpler approach is:
function ProductList({ products }) {
const activeProducts = products.filter(
product => product.active
);
return (
<ul>
{activeProducts.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}Why is this better?
There is now only one source of truth.
Instead of:
products
↓
useEffect
↓
activeProducts state
↓
renderyou have:
products
↓
filter
↓
renderFewer moving parts generally means easier-to-understand code.
2. Using useEffect for Event Handling
Consider a button that sends an order:
function Checkout({ order }) {
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
sendOrder(order);
}
}, [submitted, order]);
return (
<button onClick={() => setSubmitted(true)}>
Place Order
</button>
);
}The effect is being used to respond to a user action.
The event handler is a more direct place for that behavior:
function Checkout({ order }) {
function handleSubmit() {
sendOrder(order);
}
return (
<button onClick={handleSubmit}>
Place Order
</button>
);
}The distinction is useful:
User action
↓
Event handler
Component synchronization
↓
useEffectNot every action after a state change needs an effect.
3. Forgetting Effect Dependencies
Consider:
useEffect(() => {
console.log(search);
}, []);The effect reads search, but the dependency array is empty.
This can lead to behavior that does not match what the developer expects.
When an effect depends on a reactive value, that dependency needs to be considered carefully.
For example:
useEffect(() => {
console.log(search);
}, [search]);Now the effect runs again when search changes.
Practical rule
When writing an effect, ask:
Which values from the component does this effect depend on?
Those dependencies should be handled correctly rather than deliberately hiding them.
4. Creating an Infinite Effect Loop
A particularly confusing problem occurs when an effect updates state that causes the effect to run again.
For example:
useEffect(() => {
setCount(count + 1);
}, [count]);The sequence becomes:
Effect runs
↓
count changes
↓
Component renders
↓
Effect runs again
↓
count changes
↓
RepeatThis can create an unwanted render loop.
Before updating state inside an effect, ask whether that state update is actually necessary.
5. Forgetting Cleanup
Some effects create resources that need to be cleaned up.
For example, an event listener:
useEffect(() => {
function handleResize() {
console.log(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);The returned function performs cleanup.
This pattern is important for things such as:
- Event listeners
- Timers
- Subscriptions
- External connections
- Other resources that need cleanup
Without appropriate cleanup, resources can remain active after the component no longer needs them.
6. Creating Timers Without Cleanup
Consider:
useEffect(() => {
const timer = setInterval(() => {
console.log("Checking...");
}, 5000);
return () => {
clearInterval(timer);
};
}, []);The cleanup function stops the interval.
The general pattern is:
Create resource
↓
Use resource
↓
Component no longer needs it
↓
Clean up resourceThis is especially important for long-running applications.
7. Calling Hooks Conditionally
Hooks should not be called conditionally.
Avoid:
function Profile({ loggedIn }) {
if (loggedIn) {
const [name, setName] = useState("");
}
return <div>Profile</div>;
}Instead, call the Hook at the top level:
function Profile({ loggedIn }) {
const [name, setName] = useState("");
if (!loggedIn) {
return <p>Please log in.</p>;
}
return <div>{name}</div>;
}The important idea is that the Hook call itself should not depend on a conditional branch.
8. Calling Hooks Inside Loops
The same principle applies to loops.
Avoid patterns such as:
items.forEach(item => {
const [value, setValue] = useState("");
});Hooks should be called at the top level of a component or custom Hook.
If each item needs its own state, a separate component can often provide a cleaner design:
function Item({ name }) {
const [selected, setSelected] = useState(false);
return (
<button onClick={() => setSelected(!selected)}>
{name}: {selected ? "Selected" : "Not selected"}
</button>
);
}Then:
function ItemList({ items }) {
return (
<>
{items.map(item => (
<Item key={item.id} name={item.name} />
))}
</>
);
}9. Using Stale State Values
Consider:
function Counter() {
const [count, setCount] = useState(0);
function increaseTwice() {
setCount(count + 1);
setCount(count + 1);
}
return (
<button onClick={increaseTwice}>
{count}
</button>
);
}Both updates are based on the same captured count value.
When the next state depends on the previous state, a functional update is often the appropriate approach:
function increaseTwice() {
setCount(value => value + 1);
setCount(value => value + 1);
}Now each update receives the latest state value for that update.
This pattern is particularly useful when multiple updates are triggered close together.
10. Putting Too Much State Into a Component
A component can become difficult to maintain when it contains many unrelated state variables.
For example:
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [theme, setTheme] = useState("light");
const [products, setProducts] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [notifications, setNotifications] = useState([]);Having multiple state variables is not inherently wrong.
The question is whether the component is responsible for too many unrelated concerns.
A large component can often be divided into smaller components:
Dashboard
├── Profile
├── ProductList
├── NotificationPanel
└── SettingsEach component can then manage the state relevant to its responsibility.
useState vs useEffect
A useful distinction is:
| Hook | Typical Purpose |
|---|---|
useState | Store component state |
useEffect | Synchronize with external systems |
useMemo | Cache a calculated value when useful |
useCallback | Cache a function reference when useful |
useRef | Hold a mutable value or DOM reference |
| Custom Hook | Reuse stateful logic |
The important part is not memorizing the table.
Understand why the Hook exists before deciding to use it.
A Better Way to Think About useEffect
Instead of asking:
"Where can I use
useEffect?"
Ask:
"Does this component need to synchronize with something outside its normal rendering process?"
For example:
React component
│
├── Calculate UI
│
├── Respond to user events
│
└── Synchronize with external system
↓
useEffectThis mindset can prevent a large number of unnecessary effects.
Custom Hooks for Reusable Logic
When multiple components share stateful behavior, a custom Hook can make the logic reusable.
For example:
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return width;
}A component can then use:
function Dashboard() {
const width = useWindowWidth();
return (
<p>
Window width: {width}px
</p>
);
}The component does not need to know how the resize listener is implemented.
That logic is encapsulated inside the custom Hook.
A Practical Hook Debugging Workflow
When a Hook causes unexpected behavior, avoid immediately adding another Hook to fix it.
Use this process:
Ask:
- What causes the component to render?
- What values does the Hook depend on?
- Does the Hook update state?
- Can that update trigger the Hook again?
- Does the Hook create something that requires cleanup?
- Could the logic be performed without an effect?
Common React Hooks Mistakes Checklist
Before committing a component, check:
- Hooks are called at the top level.
- Hooks are not inside conditions or loops.
- Derived values are not unnecessarily stored in state.
- Effects have appropriate dependencies.
- Effects are not being used for simple calculations.
- Event-driven actions are handled by event handlers.
- Subscriptions and listeners are cleaned up.
- Timers are cleaned up when necessary.
- Functional state updates are used when appropriate.
- Large components are split when responsibilities become unrelated.
- Custom Hooks are used when stateful logic genuinely needs reuse.
Final Takeaway
React Hooks are powerful because they let function components manage state and interact with external systems without relying on class components.
But good React code is not about using more Hooks.
It is about using the right Hook for the right problem.
Keep these principles in mind:
State → useState
External synchronization → useEffect
Expensive calculation → useMemo when justified
Stable callback reference → useCallback when justified
Mutable value or DOM reference → useRef
Reusable stateful logic → Custom HookThe biggest improvement often comes from simplifying the component rather than adding another optimization.
When a React component becomes confusing, step back and ask:
Can this logic be simpler without another Hook?
That question alone can prevent many common React bugs.







Comments (0)
Be the first to share your thoughts.