How to useEffect in React
A detailed look at how React.useEffect works
Introduction
An essential tool to writing bug-free software is to maximize the predictability of your program. One strategy is to minimize and encapsulate side effects. In layman’s terms, a side effect is defined as a procedural change that was not communicated within its scope, otherwise known as a secondary, typically undesirable effect.
In programming, a side effect is a state change that occurs outside of its local environment. Stated differently, a side effect is anything that interacts with the outside world (i.e. outside of the local function that’s being executed). Several examples include updating the DOM, making network requests, and mutating non-local variables.
Mental Model
React.useEffect can be illustrated as a tiny box living inside a component. The box has a bit of code that runs after React has rendered the components, making it the ideal location to run side-effects.
This tiny box is equipped with the ability to receive a second argument, the dependency array, which outputs three separate behaviors:
- No argument: the effect runs after each render
- Empty array: the effect runs only after the initial render, for which the return function unmounts
- An array containing values: the effect runs when the value changes and the return function will run before the new effect
Side Effects in React
Since encapsulation of side effects is extremely crucial, React comes with a built-in hook to do just that — useEffect
. As the name suggests, useEffect
allows you to perform side effects in functional components. Whenever you want to interact with the world outside of React, whether that is to manually update the DOM or make a network request, you’d reach for useEffect
.
Adding an Effect
To add a side effect to your React component, you invoke useEffect
and pass it a function that defines your side effect.
By default, React re-invokes the effect after every render. This can be illustrated through a simple counter below. In this example, useEffect
is utilized to synchronize the document’s title with our local count
state.
Playing around with the code, you’d notice that rendering always get logged before In useEffect, changing count in a split second
. By default, on every render, the effect will not run until after React has updated the DOM and the browser has painted those updates to the view. This timing is intentional, so the side effect doesn’t block updates to the UI.
If I were to step through the sequence of state changes and re-renders in our example, it would look like this.
Initial Render
numberOfAvocados: 0
Effect (runs after render):
() => document.title = `Number of Avocados: 0`
UI Description: - 0 +
React: updates the DOM
Browser: Re-paints with DOM updates
React invokes Effect:
() => document.title = `Count: 0`User Christian clicks on the + button
React increments the count by 1, causing a re-renderNext Render
numberOfAvocados: 1
Effect (runs after render):
() => document.title = `Number of Avocados: 1`
UI Description: - 1 +
React: updates the DOM
Browser: Re-paints with DOM updates
React invokes Effect:
() => document.title = `Count: 1`User Christian clicks on the - button
React decrements the count by 1, causing a re-renderNext Render
numberOfAvocados: 0
Effect (runs after render):
() => document.title = `Number of Avocados: 0`
UI Description: - 0 +
React: updates the DOM
Browser: Re-paints with DOM updates
React invokes Effect:
() => document.title = `Count: 0`
Knowing how to add side effects to our React components, for when the initial render and every subsequent render after, let’s utilize an extremely simplified version of how Twitter may make an API request in their functional component.
The whole purpose is to display a user’s profile information, tweets, and more when visiting their page. The first step is to create a piece of state that represents the user’s profile. We’ll use the useState
hook.
The next step is to make a network request, which is our side effect. To manage our side effect, we’ll integrate it with React’s useEffect
hook. For simplicity’s sake, the twitter username will be hardcoded in.
This is a good implementation to understand useEffect
, but something is wrong with the code. Can you think of the issue? Let’s walk through it together to identify the problem.
Initial Render
profile: null
Effect (runs after render):
getTwitterProfile('jayacados').then(setProfile)
UI Description: Loading or nonexistent profileReact: updates the DOM
Browser: Re-paints with DOM updates
React invokes Effect:
() => getTwitterProfile('jayacados').then(setProfile)setProfile invoked
React updates "profile", causing a re-renderNext Render
profile: {login: 'jayacados', name: 'Jay Acados'
Effect (runs after render):
getTwitterProfile('jayacados').then(setProfile)
UI Description: <h1>@jayacados</h1>React: updates the DOM
Browser: Re-paints with DOM updates
React invokes Effect:
() => getTwitterProfile('jayacados').then(setProfile)setProfile invoked
React updates "profile", causing a re-renderNext Render
setProfile invoked
React updates "profile", causing a re-renderNext Render
setProfile invoked
React updates "profile", causing a re-renderNext Render
setProfile invoked
React updates "profile", causing a re-renderNext Render
.... repeat infinitely
The issue is that we’ll get caught in an infinite loop, not to mention, get quickly rate limited by Twitter’s API as well. When our component gets rendered, it invokes the effect, which updates our state, triggers a re-render, invokes our effect again, which then updates our state, which triggers a re-render, invokes our effect again, which then updates our state, which triggers a re-render and etc.
To prevent the issue of being invoked upon subsequent re-renders, React exposes a way to customize this through its second argument, allowing us to invoke the effect once on the initial render.
Skipping Side Effects
Knowing the basic use case for useEffect
, let’s now apply the mental model with React’s useEffect
hook.
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 doing so, given an array of values, is that React can infer when to re-invoke the effect, based on if any of those values changed between renders.
Typically, this leads us to the three scenarios that were illustrated in the mental model — no second argument, an array of all outside values that the effect depends on, or an empty array (with the assumption that your effect is not dependent on any values).
Knowing that we can utilize the second argument to bypass an infinite loop of re-renders, let’s refactor our code to ensure that our Profile
component takes this into consideration.
Let’s add an additional layer to this example. At the current moment, we are fetching the profile for jayacados
. Let’s switch it out by including a dynamic username
prop.
By adding username
inside of our effect, we’ve introduced an outside value that it depends on. As a result, we can longer use an empty array as our second argument. We either need to get rid of the array (which will bring the infinite loop problem back) or update the dependency array with what the effects depend on, which is username
. This brings our code to its finale.
Anytime username
changes (and only when the username changes), the component re-renders, the browser re-paints the view, the effect is invoked, and the profile
state will be synchronized with the result of our API request.
Cleaning Up Side Effects
Imagine dealing with the same Twitter API that we saw earlier, but it’s now using WebSockets. Instead of making a single network request to fetch our data, we set up a listener to get notified when the data changes.
In this scenario, setting it and forgetting it won’t do. We need to ensure 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 we don’t do this, memory leaks will occur.
This brings us to the final portion of the useEffect
API that is yet to be explored — the cleanup function. If you add a return function from useEffect
, React will ensure that the function is invoked before the component is removed from the DOM. In addition, if the component is re-rendered, the cleanup function for the previous render’s effect will be invoked before re-invoking the new effect.
Here’s a practical example of what useEffect
might look like if we had an API that exposed the two methods, subscribe
and unsubscribe
.
In the given example, the cleanup function is invoked in two given scenarios. The first scenario is when the username changes before the effect is invoked with a new username. The second is before the Profile
component is removed from the DOM. In both cases, we unsubscribe
from the API and reset the profile to null
(thereby rendering a Loading…
in the UI).
Lastly, the most interesting piece to this logic is also how React prioritizes UI updates before processing any effects. If the username
were to change, React invokes the old cleanup function and then invokes the new effect. In between the effects, React updates the DOM and the browser paints the Loading…
component on the UI.
In Summary…
A side effect is a state change that is observed outside of its local environment. The most common examples of side effects are making an API request, updating the DOM, and modifying variables outside of their own scope. By default, useEffect
runs on the initial render and subsequent render but can be controlled by adding a second argument to the hook.