DEV Community

Cover image for `useEffect` with Cleanup Function in React: What It Is, When to Use It, and Why
Werliton Silva
Werliton Silva

Posted on

`useEffect` with Cleanup Function in React: What It Is, When to Use It, and Why

useEffect is one of the most commonly used hooks in React. It's designed to handle side effects - that is, actions that don't directly affect component rendering, but are still necessary, such as fetching data from an API, adding event listeners, handling timers, and more.

But did you know that useEffect can (and sometimes should) clean up its own effects? This is done using a cleanup function, which many people overlook - until strange bugs start popping up in their code.


What is a Cleanup Function?

A cleanup function is a function returned from within useEffect, and it gets executed either when the component unmounts or before the effect runs again.

Basic syntax:

useEffect(() => {
  // effect code

  return () => {
    // cleanup code
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

When to use a Cleanup function?

You should use cleanup when:

  • You add event listeners (and need remove them later).
  • You use timers like setTimeout or setInterval.
  • You open websockets, subscriptions, or any resource that must be closed.
  • You perfom asynchronous setState that could case errors on unmounted components.

When not to use a cleanup function?

  • If the effect doesn’t need to be undone or canceled (e.g., a console.log or a simple fetch), there’s no need to return a cleanup function.
  • Avoid using useEffect unnecessarily - especially with empty dependencies ([]) just to run something once. In many cases, this logic can live outside useEffect if it’s not reactive.

Benefits of proper cleanup

  • Prevents memory leaks.
  • Avoids hard-to-debug issues, like functions being triggered multiple times unexpectedly.
  • Ensures predictable behavior in your components

Practical Examples

1. Clearing a timer

useEffect(() => {
  const id = setInterval(() => {
    console.log('Running...');
  }, 1000);

  return () => {
    clearInterval(id);
    console.log('Timer cleared');
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

2. Removing event listeners

useEffect(() => {
  const handleMove = (e) => console.log(e.clientX, e.clientY);
  window.addEventListener('mousemove', handleMove);

  return () => {
    window.removeEventListener('mousemove', handleMove);
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

3. Canceling WebSocket connections

useEffect(() => {
  const socket = new WebSocket('wss://example.com/chat');

  socket.onmessage = (msg) => console.log('Message:', msg.data);

  return () => {
    socket.close();
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

4. Avoiding setState on unmounted components

useEffect(() => {
  let canceled = false;

  fetch('/api/data')
    .then(res => res.json())
    .then(data => {
      if (!canceled) setData(data);
    });

  return () => { canceled = true; };
}, []);

Enter fullscreen mode Exit fullscreen mode

Real-world examples in Big Tech

Instagram / Meta - Canceling requests on scroll

useEffect(() => {
  const controller = new AbortController();

  fetch('/api/feed', { signal: controller.signal })
    .then(res => res.json())
    .then(data => setFeed(data))
    .catch(err => {
      if (err.name !== 'AbortError') throw err;
    });

  return () => controller.abort();
}, [userId]);
Enter fullscreen mode Exit fullscreen mode

YouTube - Stopping player on unmount

useEffect(() => {
  const player = createVideoPlayer();

  return () => {
    player.destroy();
  };
}, [videoId]);
Enter fullscreen mode Exit fullscreen mode

Slack - WebSocket with reconnects

useEffect(() => {
  const socket = new WebSocket('wss://slack.example.com');

  socket.onopen = () => console.log('Connected');
  socket.onclose = () => console.log('Disconnected');

  return () => {
    socket.close();
  };
}, [workspaceId]);
Enter fullscreen mode Exit fullscreen mode

Extra: Cleanup in Next.js apps

In Next.js, cleanup becomes especially important because:

Route transitions (useRouter)

import { useRouter } from 'next/router';

useEffect(() => {
  const handleRouteChange = (url) => {
    console.log('Navigating to:', url);
  };

  router.events.on('routeChangeStart', handleRouteChange);

  return () => {
    router.events.off('routeChangeStart', handleRouteChange);
  };
}, []);

Enter fullscreen mode Exit fullscreen mode

Without cleanup, listeners can stack and fire multiple times.

Shared layout components

In _app.tsx or layout files, if you have global listeners (e.g. scroll, resize), you must clean them up - or they’ll persist across route changes.

Server-side hydration mismatches

If your effect runs logic that touches window, document or 3rd-party APIs during hydration, wrap it in useEffect and make sure it cleans up - especially if the effect depends on dynamic route params, locale or theme context.

useEffect(() => {
  const highlight = () => {
    document.querySelectorAll('code').forEach(/* ... */);
  };

  highlight();

  return () => {
    // Remove listeners or cleanup DOM changes if needed
  };
}, [locale]); // e.g., if syntax highlighting depends on language

Enter fullscreen mode Exit fullscreen mode

Summary

Scenario Use cleanup?
Timer (setTimeout/setInterval) ✅ Yes
Event listener ✅ Yes
WebSocket / subscriptions ✅ Yes
Simple fetch without state ❌ No
Console logs / animations ❌ No
Route listeners (Next.js) ✅ Yes
External script usage ✅ Yes

Final Thoughts

Cleanup functions in useEffect are like tidying up after yourself: you might not notice the mess right away - but eventually, you’ll trip over it.

Next time you write an effect, ask yourself:

Do I need to undo this later?

If yes, return a cleanup function.
Your future self (and your users) will thank you.

Top comments (6)

Collapse
 
nevodavid profile image
Nevo David

Pretty cool seeing someone actually break this down so clear. I used to skip cleanup way too much and it always got me burned later.

Collapse
 
werliton profile image
Werliton Silva

Yeah, same here. I used to ignore it too. It happens sometimes, but whenever I go back to refactor, I remember it.

Collapse
 
dotallio profile image
Dotallio

Super clear breakdown! I've had bugs from missing event listener cleanup before - are there any less obvious scenarios where cleanup bites you?

Collapse
 
werliton profile image
Werliton Silva

I wouldn't say they're less obvious, but definitely things we rarely pay attention to. Like aborting a request. I once ran a benchmark where we reduced the number of requests by 50% just by aborting them as soon as the component unmounted.

cleanup is very important.

Collapse
 
michael_liang_0208 profile image
Michael Liang

Great post. Helpful for react devs.

Collapse
 
werliton profile image
Werliton Silva

I glad hear this, thanks man

Some comments may only be visible to logged-in visitors. Sign in to view all comments.