DEV Community

Mohamed Idris
Mohamed Idris

Posted on

useEffect cleanup function?!

useEffect can impact your app’s performance, so it's important to use it wisely. Let's break down why and clarify some key concepts in React.js.

Initial Render vs Re-render

  • Initial render: This happens the first time a component is rendered to the DOM, typically when the app loads or the root component is mounted.
  • Re-render: This occurs when the component’s state or props change, prompting React to update the DOM accordingly. It uses a virtual DOM to optimize updates and apply only the necessary changes.

Example Code:

import { useState, useEffect } from 'react';

const App = () => {
  const [toggle, setToggle] = useState(false);

  return (
    <>
      <h2>useEffect cleanup function</h2>
      <button className="btn" onClick={() => setToggle(!toggle)}>
        Toggle component
      </button>

      {toggle && <AnotherComponent />}
    </>
  );
};

const AnotherComponent = () => {
  useEffect(() => {
    console.log('mounting - another component');
  }, []);

  return <h2>Another Component</h2>;
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Clicking the button toggles AnotherComponent, triggering multiple mounts (initial renders).


Performance Concerns

This setup can cause performance issues, especially with things like:

  1. Timers (setInterval): Equivalent to subscribing to an event.
  2. Event listeners: Another source of potential performance problems.

These issues arise because the side effects (e.g., timers, event listeners) aren't properly cleaned up, leading to memory leaks or unnecessary work.


Solution: useEffect Cleanup Function

To address these issues, use the useEffect cleanup function. This ensures that any side effects are properly reversed when the component unmounts or when dependencies change.

Simple Example:

useEffect(() => {
  // Effect code, like adding a timer or event listener.

  return () => {
    // Cleanup code, like clearing the timer or removing the event listener.
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

This guarantees that side effects are cleaned up when the component unmounts, improving performance.


Also, when using useEffect, you might want to check out this helpful article: You might not need an effect. It can help you decide if useEffect is truly necessary in certain scenarios.


Fetching Data with Libraries (React Query, RTK Query, SWR, Next.js)

Instead of manually handling data fetching in useEffect, libraries like React Query, RTK Query, SWR, or Next.js can simplify this process. These libraries manage data fetching, caching, and state updates efficiently.

Here’s how it looks using one of these libraries:

import { useHook } from 'library';

function Example() {
  const { data, error, isLoading } = useHook('url', fetcher);

  if (error) return <div>Failed to load</div>;
  if (isLoading) return <div>Loading...</div>;

  return <div>Hello, {data.name}!</div>;
}
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Simplified Code: No need for useEffect or manual state management for loading, error, and data states.
  • Automatic Caching: These libraries handle caching, retries, and pagination out of the box.
  • Improved Performance: Data fetching logic is optimized and separated from the UI layer.

If you're building a simple app, useEffect might still be enough. But be mindful of alternatives when your app grows in complexity.


By using libraries like React Query, RTK Query, SWR, or Next.js, data fetching becomes cleaner and more efficient, reducing boilerplate code and improving the user experience.


Credits: John Smilga’s React course.

Top comments (3)

Collapse
 
edriso profile image
Mohamed Idris

setInterval Example:

import { useState, useEffect } from 'react';

const App = () => {
  const [toggle, setToggle] = useState(false);

  return (
    <>
      <h2>useEffect cleanup function</h2>
      <button className="btn" onClick={() => setToggle(!toggle)}>
        Toggle component
      </button>

      {toggle && <AnotherComponent />}
    </>
  );
};

const AnotherComponent = () => {
  useEffect(() => {
    const intervalId = setInterval(() => {
      console.log('Hello from interval');
    }, 1000);

    // Cleanup function to clear the interval
    return () => {
      console.log('Cleanup function called - component unmounted');
      clearInterval(intervalId);
    };
  }, []);

  return <h2>Another Component</h2>;
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Problem:

When you click the toggle button multiple times, you'll notice that the interval is set many times, causing performance issues.

Solution:

The solution is to use the cleanup function within useEffect. The cleanup function will run when the component unmounts, clearing the interval and preventing multiple intervals from stacking.

The relevant code is commented out in the example. The key point is to use clearInterval(intervalId) to clean up the interval.


Screenshot for context:

Collapse
 
edriso profile image
Mohamed Idris

Cleanup Function in useEffect

The cleanup function isn't always necessary, but it’s useful in cases where side effects need to be reversed, like clearing intervals or event listeners. Here’s an example to demonstrate when the cleanup function is called:

import { useState, useEffect } from 'react';

const App = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('Component mounted or count changed');

    return () => {
      console.log('Cleanup before next effect or unmount');
    };
  }, [count]);

  return (
    <>
      <h2 onClick={() => setCount(count+1)}>useEffect cleanup function</h2>
    </>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode


Explanation:

In this example, every time the count state changes (which happens when you click the header), the cleanup function is called before the effect is re-run.

So, when you click the header:

  1. The previous effect's cleanup function is called.
  2. The component is re-rendered, and the effect is run again.

This ensures that side effects are properly cleaned up before a new effect is applied.

Collapse
 
edriso profile image
Mohamed Idris

Event Listener Example:

import { useState, useEffect } from 'react';

const App = () => {
  const [toggle, setToggle] = useState(false);

  return (
    <>
      <h2>useEffect cleanup function</h2>
      <button className="btn" onClick={() => setToggle(!toggle)}>
        Toggle component
      </button>

      {toggle && <AnotherComponent />}
    </>
  );
};

const AnotherComponent = () => {
  const someFunction = () => {
    console.log('Some logic here');
  };

  useEffect(() => {
    window.addEventListener('scroll', someFunction);

    // Cleanup function to remove the event listener when the component unmounts
    // return () => {
      // window.removeEventListener('scroll', someFunction);
    // };
  }, []);

  return <h2>Another Component</h2>;
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Problem:

When you toggle the component multiple times and click on 'Refresh event listeners', you may notice that the event listener gets attached multiple times. This is because every time the component mounts, the event listener is added again without removing the previous one.

Solution:

You can solve this by uncommenting the cleanup code inside the useEffect. The cleanup function removes the event listener when the component unmounts or before the next effect runs, ensuring that only one event listener is attached.

return () => {
  window.removeEventListener('scroll', someFunction);
};
Enter fullscreen mode Exit fullscreen mode

This keeps your app from attaching multiple event listeners.