$ emrebener
home topics react the ultimate guide to useeffect: managing side effects in functional components

The Ultimate Guide to useEffect: Managing Side Effects in Functional Components

author: emre bener read time: 4 min about: useeffect, side effect, react
published: updated: mentions: document object model, memory leak, race condition, react query, uselayouteffect

Side effects in React — data fetching, DOM manipulation, talking to external APIs — all run through useEffect in functional components. The hook lets you sync a component with external systems without giving up the predictability of the rest of your code. This post walks through how useEffect actually works. When it runs, how to control that, and how to clean up after it so you don’t leak memory or burn cycles. It’s the kind of hook you think you understand until you don’t.

1. Definition

useEffect is one of the most important hooks in React, allowing to synchronize a component with an external system.

Here, the term external system refers to any piece of code that’s not controlled by React, such as:

  • Managing timers with setInterval() and clearInterval()
  • Adding or removing event subscriptions with window.addEventListener() and window.removeEventListener()
  • Sending analytics logs
  • Performing DOM manipulation (browser DOM is considered an external system)
  • Fetching data to be displayed

Effects are an “escape hatch” from the React paradigm. They let you “step outside” of React and synchronize your components with some external system like a non-React widget, network, or the browser DOM. If there is no external system involved, you shouldn’t need an effect.

Note that effects only run on the client, and they don’t run during server rendering.

2. Reference

useEffect(setup, dependencies?)
  • setup: The function that contains your effect’s logic.
  • dependencies (optional): The list of all “reactive” values referenced inside of the setup code. The list of dependencies may include props, state, or any variables or functions that have been declared inside the component body. Note: If your linter is configured for React, it will verify that every reactive value is correctly specified as a dependency.

3. Controlling Effect Execution with Dependencies

There are three primary ways to implement the useEffect hook in React:

  1. Running the effect once on mount
  2. Running the effect on every update
  3. Running the effect on mount and when dependencies change

3.1. Running the Effect Once on Mount

Passing an empty array as a dependency list to the useEffect hook in React causes the effect to run only once, which will happen during the initial render.

useEffect(() => {  
  console.log("this will only run once")  
}, [])

The useEffect hook re-runs its callback (the setup function) whenever any value in the dependency list changes. An empty array never changes. So the effect fires once during mount and never again, no matter how many times the component re-renders.

3.2. Running the Effect on Every Update

Omit the dependency list entirely and the effect runs on every component update.

useEffect(() => {  
  console.log("this will run on every update (and on mount)")  
})

With no array at all, React has nothing to compare against between renders, so it just re-runs the setup every time, on the initial render and on every subsequent re-render caused by prop or state changes.

3.3. Running the Effect on Mount and When Dependencies Change

Pass a dependency array and the effect only re-runs when one of its values changes. This is how you keep effects from firing on every render, which is the common case in practice and the one most people reach for.

useEffect(() => {  
    console.log("this will only run when 'dep1' or 'dep2' changes (and on mount)");  
  }, [dep1, dep2])

3.4. Important Notes

  • Omit a value the effect actually reads and you get stale closures, missed updates, or a lint warning — sometimes all three.
  • The set of values that the effect closes over need to match the array exactly; mismatches are how you end up with infinite loops or effects that never re-fire when they should.

4. Cleanup Function (Optional)

The setup function can optionally return a cleanup function. React runs setup when the component mounts. On every re-render with changed dependencies, it runs the cleanup first (with the old values) and then setup again (with the new ones). And when the component unmounts, the cleanup runs one last time.

For example:

useEffect(() => {  
  console.log("side effect..")  
  
  return () => {  
    console.log("cleanup..")  
  }  
})

If we were to mount this component, update it twice and then unmount it, we would get the following output in console:

// MOUNT:  
// side effect..  
  
// UPDATE 1:  
// cleanup..  
// side effect..  
  
// UPDATE 2:  
// cleanup..  
// side effect..  
  
// UNMOUNT:  
// cleanup..

💡 The cleanup function runs with the references to the old props and state.

Note: The react strict mode runs both the callback/setup and cleanup one extra time to help you spot bugs faster.

5. Remarks on Data Fetching With useEffect

Fetching data inside effects is everywhere, especially in fully client-side projects. There are real downsides though:

  • It requires quite a bit of code to fetch data in effects that doesn’t suffer from bugs like race conditions (see https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect)
  • Fetching directly in Effects usually means you don’t preload or cache data, which is not optimal
  • Effects only run on the client, meaning the initial HTML that gets served will not have the required data, and additional fetch requests will have to be performed
  • If many components in a hierarchy are fetching data, there will be “network waterfalls”, meaning data fetches may noticably block the rendering of other components

To address these potential problems, it is recommended to implement client-side caching with tools in the likes of React QueryuseSWR, or React Router 6.4+.

6. useLayoutEffect

If your effect does something visual and the render delay shows up as a flicker, reach for useLayoutEffect instead of useEffect. The rule of thumb: if you need to block the browser from painting before the effect runs, swap useEffect for useLayoutEffect.