Esc
  • Design SystemsFrom Figma Variables to Tailwind, Without the Copy-PasteDesign tokens only help if design and code read from the same source. Here's how I set up a pipeline where Figma variables become Tailwind theme values and React props, and what I learned about naming along the way.
  • Next.jsBuilding a Like Button, From Database to DelightThe heart at the bottom of every article on this site is a tiny full-stack feature. Here's how it works, from the SQLite table and API route to optimistic updates and batched requests.
  • AnimationA Friendly Introduction to Spring PhysicsSprings make interfaces feel alive. Instead of picking a duration and an easing curve, you describe how something should move, and physics does the rest. Let's build an intuition for stiffness and damping with a live playground.
  • UI DesignDesigning Dashboards People Can Actually ScanData-heavy screens fail when every number shouts at once. A practical guide to hierarchy, tables, filters and progressive disclosure for dashboards that engineers and analysts use all day.
  • CSSUnderstanding Flexbox Alignment, Visuallyjustify-content or align-items? Main axis or cross axis? If you still guess which property to use, this interactive guide will fix that for good.
  • BrandA Brand System Is More Than a LogoA logo is the smallest part of a brand. What makes a brand recognizable is the system around it, from type and color to iconography, voice and the way the product itself behaves. Here's how I approach building one that survives contact with real products.
React

Why Your useEffect Needs a Cleanup Function

Intervals that never stop, event listeners that pile up, requests that resolve after you've left the page. A hands-on look at effect cleanup and why React Strict Mode runs your effects twice.

2 min read…

useEffect lets a component reach outside of React: start a timer, subscribe to an event, open a WebSocket. But anything you start in an effect is something that might need to be stopped later.

That's what the cleanup function is for, and forgetting it is one of the most common bugs I see in React code reviews.

A timer that never dies

Here's a tiny component that counts seconds:

JS
function Ticker() {
  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    window.setInterval(() => {
      setCount((c) => c + 1);
    }, 1000);
  }, []);

  return <p>{count}s</p>;
}

Looks fine, right? The problem shows up when the component unmounts. React removes it from the screen, but the interval keeps running forever, because nothing told it to stop.

Try it yourself. Leave the checkbox unticked, then mount and unmount the ticker a few times:

⏱ 0s

Intervals running: 0

With cleanup off, toggle the ticker a few times and watch the counter climb. Every extra one is a timer nothing can stop.

Every mount starts a new interval, and none of them are ever cleared. In a real app, that's wasted CPU at best, and confusing bugs at worst, like state updates arriving from components that no longer exist.

The fix: return a function

If your effect returns a function, React calls it before the component unmounts, and also before re-running the effect when its dependencies change:

JS
React.useEffect(() => {
  const intervalId = window.setInterval(() => {
    setCount((c) => c + 1);
  }, 1000);

  return () => {
    window.clearInterval(intervalId);
  };
}, []);

Tick the checkbox in the demo and the "running" counter stays at one, no matter how many times you toggle.

Why does Strict Mode run effects twice?

In development, React's Strict Mode deliberately mounts every component, unmounts it, and mounts it again. People often think this is a bug. It's actually a gift.

A checklist for effects

Whenever you write an effect, ask: what did I start? Then make sure the cleanup undoes it.

  • setInterval / setTimeout → clearInterval / clearTimeout
  • addEventListener → removeEventListener
  • new WebSocket() → socket.close()
  • fetch() → abort it with an AbortController
  • Third-party subscriptions → call whatever unsubscribe they return

That last pattern is worth showing, since it's easy to miss:

JS
React.useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/users/${id}`, { signal: controller.signal })
    .then((res) => res.json())
    .then(setUser)
    .catch((err) => {
      if (err.name !== "AbortError") throw err;
    });

  return () => controller.abort();
}, [id]);

Now, if id changes before the first request finishes, the stale request is cancelled, and it can never overwrite the newer data.

Effects are about synchronization: keeping something outside React in step with your component. Cleanup is the other half of that contract.

Last updated September 1, 2026

Want to know when I publish something new?

I send a short email when there's a new article or project, usually a couple of times a month. No spam, and you can unsubscribe whenever you like.