Can you explain how useEffect works in React? Specifically the dependency array.

11:34 AM

M
Logo
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:

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
Logo
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

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.

Bobby uses your API key — your limits, your speed