ARTICLE
Exploring the useEffect API with Easy Examples
From React Hooks in Action by John Larsen
Some of our React components are super-friendly, reaching out to say “hi” to APIs and services outside of React. Although they’re eternally optimistic and like to think the best of all those they meet, there are some safeguards to be followed. In this article, we’ll look at setting up side effects in ways that won’t get out of hand. In particular, we’ll explore these four scenarios:
§ Running side effects after every render
§ Running an effect only when a component mounts
§ Cleaning up side effects by returning a function
§ Controlling when an effect runs by specifying dependencies
To focus on the API we’ll create some easy component examples. First up, let’s say “Bonjour, les side-effects.”
_________________________________________________________
Take 40% off React Hooks in Action by entering fcclarsen3 into the discount code box at checkout at manning.com.
_________________________________________________________
Running side effects after every render
Say you want to add a random greeting to the page’s title in the browser. Clicking the “Say Hi” button generates a new greeting and updates the title. Three such greetings are shown in Figure 1.
The title isn’t part of the DOM, nor is it rendered by React, but the title is accessible via the document
property of the window. You can set the title like this:
document.title = "Bonjour";
Reaching out to a browser API in this way is considered a side effect and we can make that explicit by wrapping the code in the useEffect hook:
useEffect(() => {
document.title = "Bonjour";
});
Listing 1 shows a SayHello
component that updates the page title with a random greeting whenever the user clicks the “Say Hi” button.
Live: https://jhijd.csb.app, Code: https://codesandbox.io/s/sayhello-jhijd
Listing 1. Updating the browser title
import React, { useState, useEffect } from "react"; #A
export default function SayHello () {
const greetings = ["Hello", "Ciao", "Hola", "こんにちは"];
const [ index, setIndex ] = useState(0);
useEffect(() => { #B
document.title = greetings[index]; #C
});
function updateGreeting () {
setIndex(Math.floor(Math.random() * greetings.length));
}
return <button onClick={updateGreeting}>Say Hi</button>
};
#A Import the useEffect hook
#B Pass the useEffect hook a function, the effect
#C Update the browser title from inside the effect
The component uses a randomly generated index to pick a greeting from an array. Whenever the updateGreeting
function calls setIndex
, React re-renders the component (unless the index value doesn’t change). React runs the effect function within the useEffect hook after every render, updating the page title as required. Notice that the effect function has access to the variables within the component because it’s in the same scope. In particular, it uses the values of the greetings
and index
variables. Figure 2 shows how you pass an effect function as the first argument to the useEffect hook.
When you call the useEffect hook in this way, without a second argument, React runs the effect after every render. But, what if you only want to run an effect when a component mounts?
Running an effect only when a component mounts
Say you want to use the width and height of the browser window, maybe for some groovy animation effect. To test out reading the dimensions, you create a little component that displays the current width and height, like in Figure 3.
Listing 2 shows the code for the component. It reaches out to read the innerWidth
and innerHeight
properties of the window
object, and once again we use the useEffect hook.
Live: https://gn80v.csb.app/, Code: https://codesandbox.io/s/windowsize-gn80v
Listing 2. Resizing the window
import React, { useState, useEffect } from "react";
export default function WindowSize () {
const [ size, setSize ] = useState(getSize());
function getSize () { #A
return {
width: window.innerWidth, #B
height: window.innerHeight #B
};
}
useEffect(() => {
function handleResize () {
setSize(getSize()); #C
}
window.addEventListener('resize', handleResize); #D
}, []); #E
return <p>Width: {size.width}, Height: {size.height}</p>
};
#A Define a function that returns the dimensions of the window
#B Read the dimensions from the window object
#C Update the state, triggering a re-render
#D Register an event listener for the resize event
#E Pass an empty array as the dependency argument
Within useEffect
, the component registers an event listener for resize events.
window.addEventListener('resize', handleResize);
Whenever the user resizes the browser, the handler, handleResize
, updates the state with the new dimensions by calling setSize
.
function handleResize () {
setSize(getSize());
}
By calling the updater function, the component kicks off a re-render. We don’t want to keep re-registering the event listener every time React calls the component. So, how do we prevent the effect from running after every render? The trick is the empty array passed as the second argument to useEffect, as illustrated in Figure 4.
As we’ll see later in the article, the second argument is for a list of dependencies. React determines whether to run an effect by checking if the values in the list have changed after the last time the component called the effect. By setting the list to an empty array, the list will never change, and we cause the effect to run only once, when the component first mounts.
But, hang on a second, alarm bells should be ringing. We registered an event listener… we shouldn’t leave that listener listening away, like a zombie shambling in a crypt for all eternity. We need to perform some cleaning up and unregister the listener. Let’s wrangle those zombies.
Cleaning up side effects by returning a function
We must be careful not to make a mess when we set up long-running side effects like subscriptions, data requests, timers and event listeners. To avoid zombies eating our brains so our memories start to leak, or ghosts shifting the furniture unexpectedly, we should carefully undo any effects that may cause undead echoes of our actions to live on.
The useEffect hook incorporates a mechanism for cleaning up our effects; return a function from the effect. React runs the returned function when it’s time to tidy up. Listing 3 updates our window-measuring component to remove the resize listener.
Live: https://b8wii.csb.app/, Code: https://codesandbox.io/s/windowsizecleanup-b8wii
Listing 3 Resizing the window
import React, { useState, useEffect } from "react";
export default function WindowSize () {
const [ size, setSize ] = useState(getSize());
function getSize () {
return {
width: window.innerWidth,
height: window.innerHeight
};
}
useEffect(() => {
function handleResize () {
setSize(getSize());
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize); #A
}, []);
return <p>Width: {size.width}, Height: {size.height}</p>
};
#A Return a clean-up function from the effect
Because the code passes useEffect an empty array as the second argument, the effect runs only once. When the effect runs, it registers an event listener. React keeps hold of the function the effect returns and calls it when it’s time to clean up. In Listing 3, the returned function removes the event listener. Our memory won’t leak. Our brains are safe from zombie effects.
Figure 5 shows this latest step in our evolving knowledge of the useEffect hook: returning a clean-up function.
Because the clean-up function is defined within the effect, it has access to the variables within the effect’s scope. In Listing 3, the clean-up function can remove the handleResize
function because handleResize
was also defined within the same effect.
useEffect(() => {
function handleResize () { #A
setSize(getSize());
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize); #B
}, []);
#A Define the handleResize function
#B Reference the handleResize function from the clean-up function
The React hooks approach, where components and hooks are functions, makes good use of the inherent nature of JavaScript, rather than too heavily relying on a layer of idiosyncratic APIs conceptually divorced from the underlying language. That means that you need a good grasp of scope and closures to best understand where to put your variables and functions.
React runs the clean-up function when it unmounts the component. But that’s not the only time it runs it. Whenever the component re-renders, React will call the clean-up function before running the effect function, if the effect runs again. If there are multiple effects that need to run again, React will call all of the clean-up functions for those effects. Once the clean-up is finished, React will re-run the effect functions as needed.
We’ve seen the two extremes: running an effect only once and running an effect after every render. What if we want more control over when an effect runs? One more case will cover this. Let’s populate that dependency array.
Controlling when an effect runs by specifying dependencies
Figure 6 is our final illustration of the useEffect API, including dependency values in the array we pass as the second argument.
Each time React calls a component, it keeps a record of the values in the dependency arrays for calls to useEffect. If the array of values has changed from the time you made the last call, React runs the effect. If the values are unchanged, React skips the effect. This saves the effect from running when the values it depends on are unchanged, meaning that the outcome of its task will be unchanged.
Let’s look at an example. Say you have a user picker that lets you select a user from a drop-down menu. You want to store the selected user in the browser’s local storage, which would allow the page to remember the selected user from visit to visit, as shown in figure 7.
Listing 4 shows the code to achieve the desired effect. It includes two calls to useEffect, one to get any stored user from local storage, and one to save the selected user whenever that value changes.
Live: https://c987h.csb.app/, Code: https://codesandbox.io/s/userstorage-c987h
Listing 4 Using Local Storage
import React, { useState, useEffect } from "react";
export default function UserStorage () {
const [ user, setUser ] = useState("Sanjiv");
useEffect(() => {
const storedUser = window.localStorage.getItem("user"); #A
if (storedUser) {
setUser(storedUser);
}
}, []); #B
useEffect(() => { #C
window.localStorage.setItem("user", user); #D
}, [user]); #E
return (
<select value={user} onChange={e => setUser(e.target.value)}>
<option>Jason</option>
<option>Akiko</option>
<option>Clarisse</option>
<option>Sanjiv</option>
</select>
);
};
#A Read the user from local storage
#B Only run this effect when the component first mounts
#C Specify a second effect
#D Save the user to local storage
#E Run this effect whenever the user changes
The component works as expected, saving changes to local storage and automatically selecting the saved user when the page is reloaded.
But, to get a better feel for how the function component and its hooks manage all the pieces, let’s run through the steps for the component as it renders and re-renders and a visitor to the page selects a user from the list. We look at two key scenarios:
- The visitor first loads the page. There is no user value in local storage. The visitor selects a user from the list.
- The visitor refreshes the page. There is a user value in local storage.
As we go through the steps, notice how the dependency lists for the two effects determine when the effect functions run.
The visitor first loads the page
When the component first runs, it renders the drop-down list of users with Sanjiv selected. Then the first effect runs. No user is in local storage, so nothing happens. Then the second effect runs. It saves Sanjiv to local storage.
- The user loads the page.
- React calls the component.
- The useState call sets the value of
user
to Sanjiv. (It’s the first time the component has called useState, so the initial value is used.) - React renders the list of users with Sanjiv selected.
- Effect one runs but there is no stored user.
- Effect two runs, saving Sanjiv to local storage.
React calls the effect functions in the order they appear in the component code. When the effects run, React keeps a record of the values in the dependency lists, []
and ["Sanjiv"]
in this case.
When the visitor selects a new user, say Akiko, the onChange
handler calls the setUser
updater function. React updates the state and calls the component again. This time, effect one doesn’t run because its dependency list hasn’t changed, it’s still []
. But, the dependency list for effect two has changed from ["Sanjiv"]
to ["Akiko"]
so effect two runs again, updating the value in local storage.
- The user selects Akiko.
- The updater function sets the user state to Akiko.
- React calls the component.
- The useState call sets the value of
user
to Akiko. (It’s the second time the component has called useState, so the latest value, set in step 8, is used.) - React renders the list of users with Akiko selected.
- Effect one doesn’t run (
[]
=[])
. - Effect two runs (
["Sanjiv"]
!=["Akiko"])
, saving Akiko to local storage.
The visitor refreshes the page
With local storage set to Akiko, if the user reloads the page, effect one will set the user state to the stored value, Akiko, as we saw in Figure 7. But, before React calls the component with the new state value, effect two still has to run with the old value.
- The user refreshes the page.
- React calls the component.
- The useState call sets the value of
user
to Sanjiv. (It’s the first time the component has called useState, so the initial value is used.) - React renders the list of users with Sanjiv selected.
- Effect one runs, loading Akiko from local storage and calling
setUser
. - Effect two runs, saving Sanjiv to local storage.
- React calls the component (because effect one called
setUser
, changing the state). - The useState call sets the value of
user
to Akiko. - React renders the list of users with Akiko selected.
- Effect one doesn’t run (
[]
=[])
. - Effect two runs (
["Sanjiv"]
!=["Akiko"])
, saving Akiko to local storage.
In step 6, effect two was defined as part of the initial render, so it still uses the initial user
value, Sanjiv.
By including user
in the list of dependencies for effect two, we’re able to control when effect two runs: only when the value of user
changes.
Summarizing the ways to call the useEffect hook
Table 1 collects the different use cases for the useEffect hook into one place, showing how the different code patterns lead to different execution patterns.
React runs the clean-up function when the component unmounts and before re-running the effect.
That’s all for this article. If you want to see more of the book, you can check it out on our browser-based liveBook platform here.
This article was originally published here: https://freecontent.manning.com/exploring-the-useeffect-api-with-easy-examples/