Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Effect cleanup

Some effects leave something running behind them: a timer, a window event listener, a subscription. If the component leaves the page while the timer keeps ticking, you get a leak — code runs for nothing and may change the state of a component that no longer exists. To prevent this, an effect returns a cleanup function.

A timer with cleanup

Create src/components/Clock.jsx — a clock that updates every second:

// src/components/Clock.jsx
import { useEffect, useState } from 'react';
 
export default function Clock() {
  const [time, setTime] = useState(new Date());
 
  useEffect(() => {
    const timer = setInterval(() => setTime(new Date()), 1000);
 
    return () => clearInterval(timer);
  }, []);
 
  return <p>It is {time.toLocaleTimeString()}</p>;
}

Render <Clock /> in App — the time on the page changes every second. The function returned by the effect stops the timer.

When React calls cleanup

MomentWhat happens
The component leaves the pageCleanup is called
A dependency changedCleanup of the old effect first, then the new one runs
Development with StrictModeRun, clean up and run again right after mounting

Checking it on the page

Add a button to App that shows and hides the clock:

// src/App.jsx
import { useState } from 'react';
import Clock from './components/Clock.jsx';
 
export default function App() {
  const [showClock, setShowClock] = useState(true);
 
  return (
    <>
      <button onClick={() => setShowClock(!showClock)}>Clock</button>
      {showClock && <Clock />}
    </>
  );
}

When the clock is hidden, Clock is not on the page and its timer is stopped. Without cleanup the timer would keep running and errors could show up in the console.

A window event listener

Listeners added with addEventListener are removed the same way. A component showing the window width — src/components/WindowWidth.jsx:

// src/components/WindowWidth.jsx
import { useEffect, useState } from 'react';
 
export default function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
 
  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth);
    }
 
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
 
  return <p>Window width: {width}px</p>;
}

Resize the browser window — the number on the page updates. removeEventListener gets the same function that was passed to addEventListener.

What needs cleaning up

What the effect startsHow to clean up
setInterval, setTimeoutclearInterval, clearTimeout
addEventListenerremoveEventListener
A subscription to an outside sourceThe unsubscribe it provides
A fetch requestA "response no longer needed" flag, see the data loading lesson

Common mistake

Without cleanup, in development an effect with setInterval runs twice, and the counter grows by 2 per second instead of 1:

// Wrong: the timer is never stopped.
useEffect(() => {
  setInterval(() => setSeconds((value) => value + 1), 1000);
}, []);

If StrictMode reveals double work, it is a sign the effect is missing a cleanup function.

Thanks for reading!

Donate to the site — we work hard for you.