Beyond the Basics: React.useEffect in-depth

Beyond the Basics: React.useEffect in-depth

If you use React, you can't go a day without writing a useEffect. But do you really understand how it works? In this article, we'll take a closer look at useEffect, provide a troubleshooting guide, and finally find out why it runs 2 times.


useEffect(setup, dependencies?)        

The useEffect hook is used for performing side effects (not the component output directly).

Call useEffect at the top level of your component to declare an Effect:

A sample UseEffect code

Parameters

  1. setup: The function with your Effect’s logic. Your setup function may also optionally return a cleanup function. When your component is added to the DOM, React will run your setup function. After each re-render with changed dependencies, React will first run the cleanup function (if you provided one) with the old values, and then run your setup function with the new values. After your component is removed from the DOM, React will run your cleanup function.
  2. dependencies (optional): The list of all reactive values referenced inside of the setup code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. If your linter is configured for React, it will verify that every reactive value is correctly specified as a dependency. The dependency list must have a constant number of items and be written inline like [value1, value2, value3]. React will compare each dependency with its previous value using the Object.is comparison. If you omit this argument, your Effect will re-run after every re-render of the component.

Returns

useEffect returns undefined.


Caveats

  • useEffect is a Hook, so you can only call it at the top level of your component or your own Hooks. You can’t call it inside loops or conditions. If you need that, extract a new component and move the state into it.
  • When Strict Mode is on, React will run one extra development-only setup+cleanup cycle before the first real setup. This is a stress-test that ensures that your cleanup logic “mirrors” your setup logic and that it stops or undoes whatever the setup is doing. If this causes a problem, implement the cleanup function.
  • If some of your dependencies are objects or functions defined inside the component, there is a risk that they will cause the Effect to re-run more often than needed. To fix this, remove unnecessary object and function dependencies. You can also extract state updates and non-reactive logic outside of your Effect.
  • If your Effect wasn’t caused by an interaction (like a click), React will generally let the browser paint the updated screen first before running your Effect. If your Effect is doing something visual (for example, positioning a tooltip), and the delay is noticeable (for example, it flickers), replace useEffect with useLayoutEffect.
  • Even if your Effect was caused by an interaction (like a click), the browser may repaint the screen before processing the state updates inside your Effect. Usually, that’s what you want. However, if you must block the browser from repainting the screen, you need to replace useEffect with useLayoutEffect.
  • Effects only run on the client. They don’t run during server rendering.
  • If you’re not trying to synchronise with some external system, you probably don’t need an Effect.


useEffect under the hood

It's pretty straightforward:

The rendering phase

  1. JSX is rendered
  2. useEffect body is executed
  3. Cleanup function is stored in memory

The re-rendering phase

  1. JSX is rendered
  2. Stored cleanup function (if any) is executed
  3. useEffect body is executed
  4. Cleanup function is stored in memory


Most common issues

1️⃣ My Effect runs twice when the component mounts

When Strict Mode is on, in development, React runs setup+cleanup one extra time before the actual setup.

This is a stress-test that verifies that your Effect’s logic is implemented correctly. If this causes visible issues, your cleanup function is missing some logic. The cleanup function should stop or undo whatever the setup function was doing. As a rule of thumb, the user shouldn’t be able to tell the difference between a single setup call (as in production) and a setup → cleanup → setup sequence (as in development).

2️⃣ My Effect runs after every re-render

First, check that you haven’t forgotten to specify the dependency array.

If you’ve specified the dependency array but your Effect still re-runs in a loop, it’s because one of your dependencies is different on every re-render.

You can debug this problem by manually logging your dependencies to the console.

You can then right-click on the arrays from different re-renders in the console and select “Store as a global variable” for both of them. You can then use the browser console to check whether each dependency in both arrays is the same using Object.is.

When you find the dependency that is different on every re-render, you can usually fix it in one of the following ways:

  • Updating state based on previous state from an Effect
  • Removing unnecessary object dependencies
  • Removing unnecessary function dependencies
  • Reading the latest props and state from an Effect

As a last resort, wrap its creation with useMemo or useCallback (for functions).

3️⃣ My Effect keeps re-running in an infinite cycle

If your Effect runs in an infinite cycle, these two things must be true:

  • Your Effect is updating some state.
  • That state leads to a re-render, which causes the Effect’s dependencies to change.

Before you start fixing the problem, ask yourself:

  • Is your Effect is connecting to some external system (like DOM, network, a third-party widget, and so on)?
  • Why does your Effect need to set state?
  • Does it synchronise with that external system?
  • Or are you trying to manage your application’s data flow with it?

If there is no external system, consider whether removing the Effect altogether would simplify your logic.

If you’re really synchronising with an external system, think about why and under what conditions your Effect should update the state. Has something changed that affects your component’s visual output? If you need to keep track of some data that isn’t used by rendering, a ref (which doesn’t trigger re-renders) might be more appropriate. Check that your effect isn't updating the state (and triggering re-renders) more than necessary.

Finally, if your Effect is updating the state at the right time, but there's still a loop, it’s because that state update causes one of the Effect’s dependencies to change.

4️⃣ My cleanup logic runs even though my component didn’t unmount

The cleanup function runs not only during unmount, but also before each re-render with changed dependencies. Additionally, in development, React runs setup+cleanup one extra time immediately after component mounts.

If you have cleanup code without corresponding setup code, it’s usually a code smell.

Your cleanup logic should be "symmetrical" to the setup logic, and should stop or undo whatever the setup did:

A sample UseEffect code

5️⃣ My Effect does something visual, and I see a flicker before it runs

If your Effect must block the browser from painting the screen, replace useEffect with useLayoutEffect. Note that this shouldn’t be necessary for the vast majority of Effects. You’ll only need this if it’s crucial to run your Effect before the browser paint: for example, to measure and position a tooltip before the user sees it.

To view or add a comment, sign in

Insights from the community

Others also viewed

Explore topics