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.


Figure 1. Clicking the Say Hi button updates the page title with a random greeting.


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.


Figure 2. Passing an effect function 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.


Figure 3. Displaying the width and height of a window as it’s resized.


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.


Figure 4. Passing an empty dependency array causes the effect function to run once, when the component mounts.


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.


Figure 5. Return a function from the effect. React will run the function to clean up after the effect.


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.


Figure 6. When calling useEffect, you can specify a list of dependencies and return a clean-up function.


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.


Figure 7. Once you select a user, refreshing the page automatically reselects the same user.


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:

  1. The visitor first loads the page. There is no user value in local storage. The visitor selects a user from the list.
  2. 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.

  1. The user loads the page.
  2. React calls the component.
  3. 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.)
  4. React renders the list of users with Sanjiv selected.
  5. Effect one runs but there is no stored user.
  6. 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.

  1. The user selects Akiko.
  2. The updater function sets the user state to Akiko.
  3. React calls the component.
  4. 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.)
  5. React renders the list of users with Akiko selected.
  6. Effect one doesn’t run ([] = []).
  7. 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.

  1. The user refreshes the page.
  2. React calls the component.
  3. 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.)
  4. React renders the list of users with Sanjiv selected.
  5. Effect one runs, loading Akiko from local storage and calling setUser.
  6. Effect two runs, saving Sanjiv to local storage.
  7. React calls the component (because effect one called setUser, changing the state).
  8. The useState call sets the value of user to Akiko.
  9. React renders the list of users with Akiko selected.
  10. Effect one doesn’t run ([] = []).
  11. 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.

Call pattern

Code pattern

Execution pattern

No second argument

useEffect(() => {

  // perform effect

});

Run after every render.

Empty array as second argument

useEffect(() => {

  // perform effect

}, []);

Run once, when the component mounts.

Dependency array as second argument

useEffect(() => {

// perform effect

// that uses dep1 and dep2

}, [dep1, dep2]);

Run whenever a value in the dependency array changes.

Return a function

useEffect(() => {

// perform effect

return () => {/* clean-up */};

}, [dep1, dep2]);

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.