|
From React Hooks in Action by John Larsen If you’re building React apps, then you’re expecting the data your app uses to change over time. Whether it’s fully server-rendered, a mobile app or all in a browser, your application’s user interface should represent the current data or state at the time of rendering. Sometimes multiple components throughout the app will use the data, and sometimes a component doesn’t need to share its secrets and can manage its own state without the help of mammoth, application-wide state-store behemoths. In this article, we’ll keep it personal and concentrate on components taking care of themselves, without regard for other components around them.
|
Take 40% off React Hooks in Action by entering fcclarsen3 into the discount code box at checkout at manning.com.
Figure 1 shows a basic illustration of React’s job: it should use the current state to render the UI. If the state changes, React should re-render the UI. The illustration shows a name in a friendly message. When the name value changes, React updates the UI to show the new name in its message. We usually want the state and UI to always be in sync (although we might choose to delay synchronization during state transitions – when fetching the latest data, for example).
Figure 1. When you change a value in a component, React should update the UI.
React provides a small number of functions, or hooks, to enable it to track values in your components and keep the state and UI in sync. For single values it gives us the useState hook and that’s the hook we’ll explore in this article. We’ll look at how to call the hook, what it returns, and how we use it to update the state, triggering React to update the UI. It’s not just a matter of documenting the useState API – you can go to the official React docs for that – we’ll use the discussion of the useState hook to help us better understand what function components are and how they work. To that end, we finish the article with a review of some key concepts.
A bookings manager
Your fun but professional company has a number of resources that can be booked by staff: meeting rooms, AV equipment, technician time, table football and even party supplies. One day, the boss asks you to create a component for the company network that lets staff select a resource to book in a booking application. The component should display a list of resources, or bookables, filtered by group, and highlight the one currently selected. When a user selects a bookable, its details can be displayed, as shown in figure 2.
Figure 2. The Bookables component displays a list of bookable items filtered by group and the details of the selected bookable.
You can find the full code examples for the bookings example app on GitHub at https://github.com/jrlarsen/react-hooks-in-action, with branches set up for each evolution of the code. Each listing for the example app in this article includes the name of the branch to checkout linked to the GitHub repo. For example, if you’ve cloned the repo, to get the code for the first branch, enter the command:
git checkout 301-hard-coded
Adding a database file
Our application will eventually need or generate a few different types of data, including users and bookings. We’ll manage all of the data in a single JSON file, db.json
. For now, we just need some bookables to show in a list, so the initial data file isn’t too complicated, as you can see in listing 1.
Branch: 301-hard-coded, File: /src/db.json
Listing 1. The bookings app data
{ "bookables": [ #A { "id": 1, "group": "Rooms", "title": "Meeting Room", "notes": "The one with the big table and interactive screen." }, { #B "id": 2, #B "group": "Rooms", #B "title": "Lecture Hall", #B "notes": "For more formal 'sage-on-the-stage' presentations" #B }, #B { "id": 3, "group": "Rooms", "title": "Games Room", "notes": "Table tennis, table football, pinball!" }, { "id": 4, #C "group": "Rooms", #C "title": "Lounge", #C "notes": "A relaxing place to hang out." #C }, { "id": 5, "group": "Kit", "title": "Projector", "notes": "Portable but powerful. Keep it with the case." }, { "id": 6, "group": "Kit", "title": "Wireless mics", "notes": "Really handy but don't forget to switch them off." } ] }
#A Assign an array of bookables data to the bookables property
#B Specify each bookable as an object
#C Give id, group, title and notes properties to each bookable
The bookables are stored in an array of bookable objects, assigned to the bookables
property. Each bookable has id
, group
, title
and notes
properties. The data in the book’s code repo has slightly longer notes but the structure is the same.
Storing, using and setting values with useState
Your React applications look after some state: values that are shown in the user interface, or that help manage what is shown. The state may include posts on a forum, comments for those posts and whether the comments are shown or not, for example. When users interact with the app, they change its state. They may load more posts, toggle whether comments are visible or add their own comments. React is there to make sure the state and the UI are in sync. When the state changes, React needs to run the components that use that state. The components return their UI using the latest state values. React then compares the newly returned UI with the existing UI and efficiently updates the DOM as necessary.
Some state is shared across the application, some by a few components and some is managed locally by a component itself. If components are just functions, how can they persist their state across renders? Are their variables not lost when they finish executing? And how does React know when the variables change? If React is faithfully trying to match the state and the UI, it definitely needs to know about changes to the state, right?
The simplest answer to the problem of persisting state across calls to your components and to the need to keep React in the loop when you change a component’s state is the useState hook. The useState hook is a function that enlists React’s help in managing state values. When you call the useState hook, it returns both the latest state value and a function for updating the value that keeps React in the loop and lets it do its syncy business.
Calling useState returns a value and an updater function
We want to alert React that a value used within a component has changed so it can re-run the component and update the UI if necessary. Just updating the variable directly won’t do. We need a way of changing that value, some kind of updater function, that triggers React to call the component with the new value and get the updated UI, as shown in figure 3.
Figure 3. Rather than changing a value directly, we call an updater function. The updater function changes the value and React updates the display with the recalculated UI from the component.
In fact, to avoid our component state value disappearing when the component code finishes running, we can get React to manage the value for us. That’s what the useState hook is for. Every time React calls our component to get hold of its UI, the component can ask React for the latest state value and for a function to update the value. The component can use the value when generating its UI and use the updater function when changing the value, for example in response to a user clicking an item in a list.
Calling useState
returns a value and its updater function as an array with two elements, as shown in figure 4.
Figure 4. The useState function returns an array with two elements: a value and an updater function.
You could assign the returned array to a variable, and then access the two elements individually, by index, like this:
const selectedRoomArray = useState(); #A const selectedRoom = selectedRoomArray[0]; #B const setSelectedRoom = selectedRoomArray[1]; #C
#A The useState function returns an array
#B The first element is the value
#C The second element is the function for updating the value
But, it’s more common to use array destructuring and assign the returned elements to variables in one step:
const [ selectedRoom, setSelectedRoom ] = useState();
Array destructuring lets us assign elements in an array to variables of our choosing. The names selectedRoom
and setSelectedRoom
are arbitrary and our choice, although it’s common to start the variable name for the second element, the updater function, with set
. The following would work just as well:
const [ myRoom, updateMyRoom ] = useState();
If you want to set an initial value for the variable, pass the initial value as an argument to the useState function. When React first runs your component, useState will return the two-element array as usual but will assign the initial value to the first element of the array, as shown in figure 5.
Figure 5. When the component first runs, React assigns the initial value you pass to useState to the ‘selected’ variable.
The first time the following line of code is executed within a component, React returns the value “Lecture Hall”
as the first element in the array. The code assigns that value to the selected
variable.
const [ selected, setSelected ] = useState("Lecture Hall");
Let’s get the Bookables
component to use the useState hook to ask React to manage the value of the selected item’s index. We’ll pass it 1
as the initial index. You should see the Lecture Hall highlighted when the Bookables
component first appears on the screen, as shown in figure 6.
Figure 6. The Bookables component with Lecture Hall selected.
Listing 2 shows the code for the component. It includes an onClick
event handler that uses the updater function assigned to setBookableIndex
to change the selected index when a user clicks a bookable.
Branch: 303-set-index, File: /src/components/Bookables.js
Listing 2. Triggering an update when changing the selected room
import React, { useState } from "react"; #A import {bookables} from "../db.json"; export default function Bookables () { const group = "Rooms"; const bookablesInGroup = bookables.filter(b => b.group === group); const [ bookableIndex, setBookableIndex ] = useState(1); #B return ( <ul className="bookables"> {bookablesInGroup.map((b, i) => ( <li key={b.title} className={i === bookableIndex ? "selected" : null} #C onClick={() => setBookableIndex(i)} #D > {b.title} </li> ))} </ul> ); }
#A Import the useState hook
#B Call useState and assign the returned state value and updater function to variables
#C Use the state value when generating the UI
#D Use the updater function to change the state value
React runs the Bookables
component code, returning the value for bookableIndex
from the call to useState. The component then uses that value when generating the UI, to set the correct className
attribute for each li
element. When a user clicks on a bookable, the onClick
event handler uses the updater function, setBookableIndex
, to tell React to update the value it’s managing. If the value has changed, React knows it’ll need a new version of the UI. So, React runs the Bookables
code again, assigning the updated state value to bookableIndex
, letting the component generate the updated UI. React can then compare the newly generated UI to the old version and decide how to update the display efficiently.
With useState, React is now listening. I don’t feel so lonely anymore. It’s living up to its promise of keeping the state in sync with the UI. The Bookables
component describes the UI for a particular state and provides a way for users to change the state. React then does its magic, checking if the new UI is different from the old (diffing), batching and scheduling updates, deciding on an efficient way to update DOM elements and then doing the deed and reaching out to the DOM on our behalf. We fixate on the state, React does its diffing and updates the DOM.
In listing 2 we passed an initial value of 1 to useState. A user clicking on a different bookable replaces that value with another number. But what if we want to store something more complicated, like an object, as state? In that case, we need to be a bit more careful when updating the state. Let’s see why.
Calling the updater function replaces the previous state value
If you’re coming from the class-based approach to component building in React then you’ll be used to state being an object with different properties for different state values. Moving to function components, you may try and replicate that state-as-an-object approach. It may feel more natural to have a single state object and have new state updates merge with the existing state. But the useState hook is easy to use and easy to call multiple times, once for each state value you want React to monitor. It’s worth getting used to separate calls to useState for each state property. If you really need to work with objects as state values, you should be aware of how setState
as a function component updater function is different from this.setState
you’d use with a class component. In this section we take a brief look at updating the state of an object in the two types of components.
The class component approach
With classes, you would set up the state as an object in the constructor, (or as a static property on the class):
class Bookables extends React.Component { constructor (props) { super(props); this.state = { bookableIndex: 1, group: "Rooms" }; } }
To update the state, in an event handler say, you would call this.setState
, passing an object with any changes you want to make:
handleClick (index) { this.setState({ bookableIndex: index }); }
React would merge the object you passed to setState
with the existing state. In the example above, it would update the bookableIndex
property but leave the group
property alone, as shown in figure 7.
Figure 7. In a class component, calling the updater function (this.setState) merges the new properties with the existing state object.
The function component approach
In contrast, for the new hooks approach, the updater function replaces the previous state value with the value you pass to the function. Now, that’s straightforward if you have simple state values, like this:
const [bookableIndex, setBookableIndex] = useState(1); setBookableIndex(3); // React replaces the value 1 with 3.
But, if you’ve decided to store JavaScript objects in state, you’ll need to tread carefully. The updater function will replace the old object entirely. Say you initialize the state like this:
function Bookables () { const [state, setState] = useState({ bookableIndex: 1, group: "Rooms" }); }
If you call the updater function, setState
, with just the changed bookableIndex
property:
function handleClick (index) { setState({ bookableIndex: index }); }
then you’ll lose the group
property. The old state object is replaced by the new one, as shown in figure 8.
Figure 8. In a function component, calling an updater function (returned by useState) replaces the old state value with whatever you pass to the updater function.
So, if you really need to use an object with the useState hook, you’ll need to copy across all the properties from the old object when you set a new property value:
function handleClick (index) { setState({ ...state, bookableIndex: index }); }
Notice how the spread operator, ...state
, is used in the snippet above to copy all of the properties from the old state to the new. In fact, to ensure you have the latest state when setting new values based on old, you can pass a function as the argument to the updater function, like this:
function handleClick (index) { setState(state => { return { ...state, bookableIndex: index }; ); }
React will pass in the latest state as the first argument.
Reviewing some function component concepts
At this point, our Bookables
component is very simple. But there are already some fundamental concepts at work, concepts that underpin our understanding of function components and React hooks. Having a strong grasp of these concepts will make future discussions and your expert use of hooks much easier. In particular, here are five key concepts:
In order to discuss concepts with clarity and precision, from time to time we’ll take stock of the key words and objects we’ve encountered so far. Table 1 lists and describes some of the terms we’ve come across:
Icon |
Term |
Description |
|
Component |
A function that accepts props and returns a description of its UI. |
|
Initial value |
The component passes this value to useState. React sets the state value to this initial value when the component first runs. |
|
Updater function |
The component will call this function to update the state value. |
|
Event handler |
A function that runs in response to an event of some kind. For example, a user clicking a bookable. Event handlers often call updater functions to change the state. |
|
UI |
A description of the elements that make up a user interface. The state values are often included somewhere in the UI. |
The component cycle diagram in figure 9 shows some of the steps involved when our Bookables
component runs and a user clicks a bookable. Its accompanying table discusses each step.
Figure 9. Stepping through the key moments when using useState
Step |
What happens? |
Discussion |
1 |
React calls the component. |
To generate the UI for the page, React traverses the tree of components, calling each one. React will pass each component any props set as attributes in the JSX. |
2 |
The component calls |
The component passes the initial value to the |
3 |
React returns the current value and an updater function as an array. |
The component code assigns the value and updater function to variables for later use. The second variable name often starts with |
4 |
The component sets up an event handler. |
The event handler may listen for user clicks, for example. The handler will change the state when it runs later. React will hook up the handler to the DOM when it updates the DOM in step 6. |
5 |
The component returns its UI. |
The component uses the current state value to generate its user interface and returns it, finishing its work. |
6 |
React updates the DOM |
React updates the DOM with any changes needed. |
7 |
The event handler calls the updater function. |
An event fires and the handler runs. The handler uses the updater function to change the state value. |
8 |
React updates the state value. |
React replaces the state value with the value passed by the updater function. |
9 |
React calls the component. |
React knows the state value has changed so must recalculate the UI. |
10 |
The component calls useState for the second time. |
This time, React will ignore the initial value argument. |
11 |
React returns the current state value and the updater function. |
React has updated the state value. The component needs the latest value. |
12 |
The component sets up an event handler. |
This is a new version of the handler and may use the newly updated state value. |
13 |
The component returns its UI. |
The component uses the current state value to generate its user interface and returns it, finishing its work. |
14 |
React updates the DOM |
React compares the newly returned UI with the old and efficiently updates the DOM with any changes needed. |
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.