React useEffect Explained: When Effects Execute

React useEffect Explained: When Effects Execute


4 min read

React's useEffect hook is an important tool for managing side effects in your functional components.

In this post, we'll look at when your effects run.

Effects run on the initial render

Every effect runs at least once when your component first renders. In the example below, the log statement in the useEffect hook executes without you having to do anything:

Effects run every render if there are no dependencies

In the previous example, we passed an empty array as the second argument to the useEffect hook. This argument is called the dependency array. The array controls when the effect runs after the first render.

If you don't pass a second argument to the useEffect hook, your effect will run on every render. In the example below, the log statement runs every time you click the Force Rerender button:

Effects run when dependencies change

If you want to be more selective about when your effect runs, you need to use the dependency array argument. The way it works is that the values in the dependency array are compared every render. If a value at a given position changes, the effect is executed again.

Let's take a look at an example:

// First render
useEffect(() => {}, [1, 2]);

// Second render
useEffect(() => {}, [1, 3]);
  1. Dependency array on the first render: [1, 2]

  2. Dependency array on the second render: [1, 3]

The first element (1) is the same between renders, but the second element (2 and 3) is different. The effect is re-executed because the value changed for the second element in the dependency array.

In the example below, we have a count variable managed by React state. Every time you click the Count button, the value is incremented, and the component re-renders.

Since the count variable passed to the useEffect dependency array changes between renders, the log statement will run.

Now what if we click the Force Rerender button instead?

This button doesn't update the count variable. Even though the component re-renders, the value passed to the useEffect dependency array is the same. In this case, the log statement won't run.

Try it out!

Non-primitive data types are compared by reference

One thing to keep in mind is that any non-primitive data type passed to a dependency array is compared by its reference.

This means that even if a value looks the same, if the underlying reference changes, React will execute the effect.

For example, the two functions below may look the same, but from JavaScript's perspective, they have two different function references:

const fnOne = () => true;
const fnTwo = () => true;
fnOne === fnTwo // false (different references)

Here are a few data types where this applies:

  • Functions

  • Objects

  • Arrays

We can see this play out in the example below.

Since the logger function is recreated every render, the underlying reference will be different.

This time, since we pass the logger in the dependency array, the Force Rerender button will cause the log statement to run again:

Create stable references to prevent effects from running

We can pass stable references to our dependency arrays to solve this problem.

For functions that are created in a React component, this usually means wrapping it with the useCallback hook.

The useCallback hook takes a function as its first argument and a dependency array as the second argument. On the initial render, a new function is created. However, every other render will return the same function if the elements in the dependency array are the same.

The useCallback dependency array follows the same rules as the useEffect dependency array.

This time the logger function is wrapped with the useCallback hook, so we get a stable reference between renders and the Force Rerender button won't cause the log statement to run again:

Effects can have cleanup functions

Another important part about effects is that they have optional cleanup functions. If you return a function in an effect, React will execute it before executing the next effect. In other words, it'll look something like this:

  1. The effect runs on the initial render

  2. A value in the dependency array changes

  3. The cleanup function from the first effect runs

  4. The next effect runs and returns a new cleanup function

In the example below, you'll notice that a Clean up Count log statement runs before the next Count log statement every time you click the Count button.

And that's it!

A quick look at when React's useEffect hook runs.

Understanding when the useEffect hook runs will help you manage side effects predictably and efficiently, ensuring that your React components behave as expected!