Can you explain how useEffect works in React? Specifically the dependency array.
11:34 AM
M
Bobbyclaude-3-5-sonnet11:34 AM
Alright, let's talk useEffect — the hook that trips up more developers than any other. Here's the deal:
The three modes:
The stale closure gotcha (this will bite you):
Bottom line: the dependency array is a declaration of what your effect reads, not a performance optimization. Include everything you use inside the effect. The ESLint rule
The three modes:
tsx
// Mode 1: Runs after EVERY render. Usually a mistake.
useEffect(() => {
console.log('I run constantly. Hope you like logs.');
});
// Mode 2: Runs ONCE on mount. The "componentDidMount" equivalent.
useEffect(() => {
fetchUserData();
return () => cleanup(); // runs on unmount
}, []);
// Mode 3: Runs when deps change. The one you actually want.
useEffect(() => {
document.title = `User: ${userId}`;
}, [userId]); // only re-runs when userId changes
The stale closure gotcha (this will bite you):
tsx
const [count, setCount] = useState(0);
// ❌ This is broken — count is always 0 inside here
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // stale closure!
}, 1000);
return () => clearInterval(id);
}, []); // empty deps = never re-subscribes
// ✅ Use the functional updater form instead
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1); // reads fresh value
}, 1000);
return () => clearInterval(id);
}, []);
Bottom line: the dependency array is a declaration of what your effect reads, not a performance optimization. Include everything you use inside the effect. The ESLint rule
exhaustive-deps is your friend.What about cleanup functions? When exactly do they run?
11:36 AM
M
Bobbyclaude-3-5-sonnet11:36 AM
Cleanup functions run in exactly two situations:
1. Before the next effect runs — if your effect re-runs because a dep changed, the cleanup from the previous run fires first
2. When the component unmounts — final cleanup
This pattern is *chef's kiss* for preventing race conditions. User clicks fast → each new effect cancels the previous fetch. No stale data.
1. Before the next effect runs — if your effect re-runs because a dep changed, the cleanup from the previous run fires first
2. When the component unmounts — final cleanup
tsx
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
return () => {
controller.abort(); // cancels in-flight request on cleanup
};
}, [userId]); // re-fetches when userId changes, cancels previous fetch
This pattern is *chef's kiss* for preventing race conditions. User clicks fast → each new effect cancels the previous fetch. No stale data.
Shift+Enter for newline
Bobby uses your API key — your limits, your speed