Skip to the solution
MediumFrontend7 min read

How do you handle memory leaks in React?

724 views

The problem

Question presented to candidate: "A long-running React app gets slower the more the user navigates around. What kinds of memory leaks does React make easy to write, and how do you prevent them?"

What a strong answer should cover:

  • The single unifying rule: every effect that sets something up must return a cleanup that tears it down.
  • The usual culprits: event listeners, setInterval/setTimeout, WebSocket and observer subscriptions, and in-flight requests.
  • Cleanup runs before the next effect run and on unmount β€” that ordering is what makes it work.
  • AbortController for fetches; a cancellation flag when the API cannot be aborted.
  • Observers (IntersectionObserver, ResizeObserver, MutationObserver) need explicit disconnect().
  • A subtle one: a closure captured in a long-lived subscription keeps its entire scope alive, including large objects.
  • StrictMode makes this visible in development by mounting, cleaning up, and remounting β€” an effect that leaks accumulates immediately.
  • Modern nuance: since React 18 a setState on an unmounted component is a silent no-op, so the old "can't perform a React state update on an unmounted component" warning is gone. Its absence does not mean there is no leak.
  • Diagnosis: DevTools Profiler for render cost, Chrome Memory panel heap snapshots and the detached-DOM-node check for actual retention.

Clarifying questions expected:

  • "Is memory actually growing, or is it a re-render performance problem?" β€” different diagnosis entirely.
  • "Does it get worse with navigation, or over time on one screen?"

Code / implementation expected: Yes β€” an effect with a cleanup, and the AbortController fetch pattern.

  • performance
  • hooks
  • debugging

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 10 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactA leaking effect beside a clean one, with a live listener count
import { useState, useEffect, useRef } from "react";

// Instrument the real APIs so the leak is visible rather than theoretical.
let attached = 0;
const origAdd = window.addEventListener.bind(window);
const origRemove = window.removeEventListener.bind(window);
window.addEventListener = (...args) => { attached++; return origAdd(...args); };
window.removeEventListener = (...args) => { attached--; return origRemove(...args); };

// ❌ Leaks: no cleanup returned, so the listener outlives every unmount.
function Leaky() {
  const [w, setW] = useState(window.innerWidth);
  useEffect(() => {
    window.addEventListener("resize", () => setW(window.innerWidth));
    // no return β€” nothing is ever removed
  }, []);
  return <span>width {w}</span>;
}

// βœ… Clean: the SAME function reference is added and removed. Two inline
// arrows would be two different functions and would remove nothing.
function Clean() {
  const [w, setW] = useState(window.innerWidth);
  useEffect(() => {
    const onResize = () => setW(window.innerWidth);
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);
  return <span>width {w}</span>;
}

// The fetch case: abort cancels the request itself, not just the state write.
function AbortDemo() {
  const [status, setStatus] = useState("idle");
  useEffect(() => {
    const controller = new AbortController();
    setStatus("requesting…");
    fetch("https://example.com/slow", { signal: controller.signal })
      .then(() => setStatus("done"))
      .catch((e) => setStatus(e.name === "AbortError" ? "aborted on unmount" : "failed (expected offline)"));
    return () => controller.abort();
  }, []);
  return <span>fetch: {status}</span>;
}

export default function App() {
  const [showLeaky, setShowLeaky] = useState(false);
  const [showClean, setShowClean] = useState(false);
  const [showFetch, setShowFetch] = useState(false);
  const [, force] = useState(0);
  const cycles = useRef({ leaky: 0, clean: 0 });

  const row = { display: "flex", gap: 10, alignItems: "center", marginBottom: 10 };

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6 }}>
      <p style={{ background: "#fff3cd", padding: 10, borderRadius: 6 }}>
        Listeners currently attached to window: <strong>{attached}</strong>{" "}
        <button onClick={() => force((n) => n + 1)}>refresh count</button>
      </p>

      <div style={row}>
        <button onClick={() => { if (!showLeaky) cycles.current.leaky++; setShowLeaky(!showLeaky); }}>
          {showLeaky ? "Unmount" : "Mount"} Leaky ({cycles.current.leaky} mounts)
        </button>
        {showLeaky && <Leaky />}
      </div>

      <div style={row}>
        <button onClick={() => { if (!showClean) cycles.current.clean++; setShowClean(!showClean); }}>
          {showClean ? "Unmount" : "Mount"} Clean ({cycles.current.clean} mounts)
        </button>
        {showClean && <Clean />}
      </div>

      <div style={row}>
        <button onClick={() => setShowFetch(!showFetch)}>
          {showFetch ? "Unmount" : "Mount"} AbortDemo
        </button>
        {showFetch && <AbortDemo />}
      </div>

      <p style={{ color: "#666", fontSize: 13 }}>
        Mount and unmount Leaky several times, then hit refresh count β€” it climbs
        and never comes back down. Do the same with Clean and it always returns
        to where it started.
      </p>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 51 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

How do you prevent unnecessary re-renders in functional components?

Continue