Understanding the useEffect hook in React. Designing custom hooks

JavaScript React

Hooks are quite a new feature in React. They seem to simplify how we add logic to our components. When I was starting to use them, most of them seemed straightforward. That didn’t apply to the useEffect hook. It might not be that uncomplicated, and therefore, it needs more explanation. In this article, we look into React useEffect and explain some of its caveats. We also learn how it can be used to substitute for the lifecycle methods.

The purpose of the useEffect hook

Putting it simply, the useEffect lets us perform side effects in a component. An example of such might be fetching some data and listening for upcoming events.

Let’s look into an elementary example of using effects:

The code above does work, and we get a list of todos from the jsonplaceholder. There is a significant problem, though: the useEffect hook runs on every render, by default. It acts as the combination of the componentDidMount and componentDidUpdate methods.

useEffect dependencies

To customize the above behavior, we can use an array of dependencies as a second argument of the useEffect hook. With it, the callback runs only if some of the elements of the array change.

Since we want our hook to run only once, we can pass an empty array. This works similar to the componentDidUpdate method.

There are often times when we do have some dependencies, and we want to run our effects more than once. A very common example is the one with modifying  .

If we use the code above, it runs on every render. We don’t really need that!

If we pass an empty array, two things happen. First, we would always see a message: “Fetched 0 todos”. That’s because it runs only once before we successfully fetch any todos.

The second thing is that we see a warning coming from the   eslint rule.

A solution is to add the   to dependencies.

Now our useEffect callback runs every time   changes.

Putting the useCallback hook to use

The above is a straightforward example, but the dependencies are not always that obvious. Let’s complicate things a bit on purpose by introducing a function that isn’t pure.

If you want to know more about pure functions, check out Improving our code with pure functions

Now we see a warning, and the   does not update.

The above is not very accurate. Adding   to dependencies won’t fix the error, and we still end up with a warning.

The ‘countCompletedTodos’ function makes the dependencies of useEffect Hook (at line 10) change on every render. Move it inside the useEffect callback. Alternatively, wrap the ‘countCompletedTodos’ definition into its own useCallback() Hook react-hooks/exhaustive-deps

We can either put the   in the dependencies or follow the advice above and use the useCallback hook. It returns a memoized version of the callback that only changes if one of the provided dependencies change.

Now,   changes only if   change.

Designing custom hooks

Above, we use a custom hook. We can do the same with our previous code to make it more elegant.

When creating a custom hook similar to the one above, we distinct the logic from a template. Thanks to that, we follow the separation of concerns principle.

There is a question that often pops up when creating a custom hook, such as the one above: should we return an object or an array? The hooks like useState use the latter, and it might seem like a standard approach. The above is not always the case: the useContext hook returns an object, for example.

The main advantage of returning an array is the fact that we can define the names of the elements of the array when destructuring.

It works very well with very generic hooks like the useState. Since our custom hook is quite specific and returns a few different properties, it might be better to return an object. Now, when we use the   hook, we don’t need to dwell on the order of the properties.

Cleaning up after a side effect

When performing a side-effect, we often need to clean up afterward. A good example of it is subscribing to some events. When our component unmounts, we would like to unsubscribe.

In order to perform a cleanup, our useEffect callback needs to return a function.

Now, our   callback is removed from the window as soon as the   component unmounts.

The above is similar to using the componentWillUnmount method. There is a significant difference, though. Our useEffect hook might run multiple times before unmounting. Because of that, React also cleans up before running the effects next time. To observe this, let’s modify one of our previous examples.

When we run the above code in our  we can observe the   text in the console right away. This is because our useEffect callback runs twice.

First, it happens with no todos when   is equal to 0. It occurs for the second time when the todos are successfully fetched.


In this article, we’ve gone through some of the common use-cases of the useEffect hook. When doing so, we’ve encountered some issues that we’ve sorted out. Throughout all of the above, we’ve learned how to use the useEffect hook instead of the lifecycle methods and how it differs from them. Also, to make our code more elegant by separating the logic from the template in the form of custom hooks. What is your approach to creating them?

Notify of
Newest Most Voted
Inline Feedbacks
View all comments
Łukasz Wójcik
Łukasz Wójcik
4 years ago

I was always to lazy to check… but if you call setState twice one after another… doesn’t this trigger re-render twice? Or hooks are different about this?

Lance Cameron Kidwell
4 years ago

state updates are bundled together, so setState multiple times in one render cycle will be queued for the next

3 years ago

There is a mistake in the guide.
This works similar to the componentDidUpdate method.”
If you pass an empty array as a second argument in useEffect, the behaviour is similar to componentDidMount