Side Effects (useEffect)

The key to writing bug-free software is to maximize the predictability of your program. One strategy to do that is to minimize and encapsulate the side effects in your program. In English, a side effect is defined as "a secondary, typically undesirable effect." Related, in programming, a side effect is a state change that can be observed outside of its local environment. Said differently, a side effect is anything that interacts with the outside world (in our case, "outside" meaning outside of the local function that's being executed). Mutating non-local variables, making network requests, and updating the DOM are all examples of common side effects.

Side Effects in React

Because the encapsulation of side effects is so crucial, React comes with a built-in Hook to do just that called **useEffect**. As the name suggests, **useEffect** allows you to perform side effects in function components. Per our definition earlier, whenever you want to interact with the world outside of React (whether that's to make a network request, manually update the DOM, etc.), you'd reach for **useEffect**.

There are three aspects to the **useEffect** API that are important to understand - how to add an effect, how to skip re-invoking the effect, and how to (optionally) clean up that effect.

Add an effect

To add a side effect to your React component, you invoke **useEffect** passing it a function which defines your side effect.

By default, React will re-invoke the effect after every render. We can see this by looking at a simple "Counter" example. Here we're using **useEffect** to synchronize the document's title with our local **count** state.

If you were to play around with the code above, you'd notice that **Rendering** always gets logged before **In useEffect, after render**. Again, by default, on every render (including the initial render), the effect won't run until after React has updated the DOM and the browser has painted those updates to the view. The reason for this timing is so the side effect doesn't block updates to the UI.

If we were to step through this sequence of state changes and re-renders in our Counter app, it would look like this.


At this point, we've learned both how to add side effects to our React components and when they run, after every render. Knowing what you know now, how would you approach making an API request in your function component? For example, say you were given this **getGithubProfile** function which fetched profile data from the Github API.

How would you implement a **Profile** component which was responsible for displaying information about a Github user's profile information?

Here's a codesandbox where you can attempt it for yourself. Don't just scroll down and go to the solution! Get your hands dirty.

. . . . . .

Here's the approach you probably took. First, since the whole purpose of our component is to display profile information about a user, we'll want to have a piece of state that represents that user's profile. We can use the **useState** Hook for that.

Next, we know that making a network request is a side effect. To manage our side effect, we'll put it inside of React's **useEffect** Hook. To keep things simple for now, we'll hardcode **tylermcginnis** as our Github username. While we're here, we'll also give our component some (basic) UI.

Looks pretty good, right? Unfortunately, no. Can you think of what's wrong with the code above? Like we did earlier, let's walk through the sequence of state changes and re-renders in our **Profile** component to figure out where we went wrong.


Did you spot the issue? We quickly get caught in an infinite loop (and just as quickly, get rate limited by the Github API). Our component gets rendered then invokes our effect, which updates our state, which triggers a re-render, which then invokes our effect, which updates our state, which triggers a re-render, and on and on.

What we need is a way to opt out of **useEffect**'s default functionality of being re-invoked on every re-render. In our example, since we hardcoded the username to be **tylermcginnis**, we only want to invoke the effect once on the initial render, not on every subsequent re-render. Luckily for us, **useEffect** exposes a way to customize this via its second argument.


Skipping side effects

If you pass a second argument to **useEffect**, you need to pass to it an array of all of the outside values your effect depends on. A byproduct of that is, given an array of values, React can infer when to re-invoke the effect based on if any of those values change between renders. This typically leads us to one of three scenarios - no second argument, an array of all outside values the effect depends on, or an empty array (assuming your effect doesn't depend on any values).

We'll dive deeper into effectively managing **useEffect**'s second array argument later in this course.


Now, knowing what we know about skipping effects, how can we update our **Profile**component from earlier so we don't get trapped in an infinite loop?

Previously, we mentioned that "we only want to invoke the effect once on the initial render, not on every subsequent re-render." Along with that, since we hardcoded the **username** to be **tylermcginnis**, our effect isn't dependent on any outside values. This means we can safely pass an empty array as the second argument so our effect will only be invoked on the initial render.

Now our code is perfect, assuming we only ever want to fetch the **tylermcginnis**profile. Sadly, that's now how writing real software works.

Instead of always fetching the profile for **tylermcginnis**, we probably want our **Profile** component to take in a dynamic **username** prop. That way, we let the consumer of our **Profile** component decide which profile to show.

Now the only other changes we need to make are related to our **useEffect** Hook. As a reminder, here's what we currently have.

First, we want to swap out our hardcoded **tylermcginnis** string with our dynamic **username** prop.

By adding **username** inside of our effect, we've introduced an outside value that it depends on. This means we can no longer use an empty array as our second argument. Our options are either get rid of the array (which will bring back our infinite loop problem) or update the dependency array with what the effect depends on, **username**. Naturally, we'll do the later. That brings the final code for our **Profile** component to this.

Now, anytime **username** changes (and only when **username** changes), once the component re-renders and the browser re-paints the view, our effect will be invoked and the **profile** state will be synchronized with the result of our API request.


At this point, we've learned how, using the **useEffect** Hook, we can add side effects to our React components. We've also learned how, using the second argument to **useEffect**, we can skip re-invoking an effect if none of the values it depends on changes. The last scenario we need to cover is a side effect that requires a clean-up phase, like a subscription-based API or a timer.

Cleaning up side effects

Let's imagine we're dealing with the same Github API as we saw earlier, but this time it's WebSocket based. Instead of making a single network request to get our data, we set up a listener to be notified whenever the data changes. In this scenario, we can't just set it and forget it. We need to make sure that we clean up our subscription whenever the component is removed from the DOM or when we no longer want to receive updates from the API. If not, we'll have a memory leak.

This brings us to the last part of the **useEffect** API we haven't explored yet, the cleanup function. If you return a function from **useEffect**, React will make sure that function is invoked right before the component is removed from the DOM. Additionally, if your component is re-rendered, the cleanup function for the previous render's effect will be invoked before re-invoking the new effect.

Here's an example of what this might look like if we had an API that exposed two methods, **subscribe**and **unsubscribe**.

In this example, there are two scenarios where the cleanup function would be invoked. First, whenever **username** changes, before the new effect is invoked with the new **username**. And second, right before **Profile** is removed from the DOM. In both scenarios, we want to **unsubscribe** from our API as well as reset **profile** to **null**so we get the **Loading...** UI.

As we've done a few times now, it's helpful to walk through the process of state changes and re-renders. This time especially so we can see exactly when the cleanup function will be invoked compared to our effect and re-renders.

So we can better see this process, let's assume our **Profile** component is rendered initially with the **username** **tylermcginnis**, then a user event triggers a re-render with the **username** **sdras**.

To me, the most interesting part of that flow is how React prioritizes UI updates before worrying about any of the effects. When **username** changes to **sdras**, React has to invoke the old cleanup for **tylermcginnis**, then invoke the new effect for **sdras**. However, before it does any of that, React updates the DOM and the browser repaints with the new UI.

If you haven't already, I highly recommend playing around with the code from this example and trying to guess what order the console.logs will be executed.

useEffect vs Lifecycle Events

You may have noticed that up until this point, I've been deliberate about not making comparisons between **useEffect** and the traditional component lifecycle methods (**componentDidMount****componentDidUpdate****componentWillUnmount**). There are a few reasons for that. First, it creates an unnecessary pre-requisite for someone new to React. As we've seen, you don't need to know anything about the traditional lifecycle methods to understand **useEffect**. Second, they're two fundamentally different mental models. By comparing them upfront you send a signal, often unintentionally, that **useEffect** is just a new way of adding lifecycle events to our function components. As you've seen by now, this couldn't be further from the truth.


Copyright 2023 © Borja Leiva

Made within London