React Hooks make development easier, but misusing them can lead to performance issues like unnecessary re-renders, stale closures, and memory leaks. Here are some common pitfalls and how to fix them.
useState
Updates :If you call setState
with the same value, React will still re-render the component.
function Counter() {
const [count, setCount] = useState(0);
console.log("Component re-rendered");
return <button onClick={() => setCount(0)}>Set to 0</button>;
}
* Issue: Clicking the button re-renders even though the state hasn’t changed.
* Fix : Use a function update to check the current state.
<button onClick={() => setCount(prev => (prev === 0 ? prev : 0))}>Set to 0</button>
useEffect
:useEffect
without proper dependenciesfunction App() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // Causes infinite loop
}, [count]);
return <p>Count: {count}</p>;
}
* Issue: Every time count
updates, useEffect
triggers another update.
* Fix: Use a function update or an appropriate dependency array.
useEffect(() => {
setCount(prev => prev + 1);
}, []); // Runs only once
useEffect
:useEffect
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
const log = () => console.log(count); // Recreated every render
document.addEventListener("click", log);
return () => document.removeEventListener("click", log);
}, [count]); // Memory leak risk
}
* Issue: Every re-render creates a new log
function, causing multiple event listeners.
* Fix: Use useCallback
to memoize the function.
const log = useCallback(() => console.log(count), [count]);
useEffect(() => {
document.addEventListener("click", log);
return () => document.removeEventListener("click", log);
}, [log]);
useEffect
:useEffect
useEffect(() => {
console.log(count); // May log an outdated count
}, []); // Missing [count]
* Issue : The effect captures the initial value of count
, not the latest.
* Fix : Always add dependencies to the array.
useEffect(() => {
console.log(count);
}, [count]);
useMemo
and useCallback
:useMemo
and useCallback
when unnecessaryconst computedValue = useMemo(() => a * b, [a, b]); // Unnecessary if computation is simple
const handleClick = useCallback(() => console.log("Clicked"), []); // Doesn't need memoization
* Issue : These Hooks add complexity and should be used only for expensive computations.
* Fix : Use them only when necessary (e.g., for expensive calculations or preventing unnecessary prop changes).
function Parent() {
return <Child onClick={() => console.log("Clicked")} />; // New function every render
}
* Issue : Child
re-renders every time even if onClick
doesn’t change.
* Fix : Use useCallback
to prevent unnecessary re-renders.
const handleClick = useCallback(() => console.log("Clicked"), []);
<Child onClick={handleClick} />;
useEffect(() => {
fetchData().then(data => setState(data)); // Possible memory leak
}, []);
* Issue : If the component unmounts before fetchData
completes, React tries to update an unmounted component.
* Fix : Use a cleanup function.
useEffect(() => {
let isMounted = true;
fetchData().then(data => {
if (isMounted) setState(data);
});
return () => {
isMounted = false;
};
}, []);
Bad Practice | Best Practice |
---|---|
Updating state unnecessarily | Only update if values change |
Missing dependencies in useEffect |
Always include necessary dependencies |
Recreating functions inside components | Use useCallback for stable functions |
Using useMemo everywhere |
Use it only for expensive calculations |
Updating state inside useEffect without cleanup |
Use cleanup functions to avoid memory leaks |