useMemo
hook to avoid rerunning expensive computationsuseMemo
with a dependency arrayReact is great at making it easy to display data in efficient, appealing, and responsive ways. But simply throwing raw data onto the screen is rare. Whether our apps are statistical, financial, scientific, entertaining, or whimsical, we almost always manipulate our data before bringing it to the surface.
Sometimes that manipulation can be complicated or time-consuming. If the time and resources spent are necessary to bring the data to life, the outcome may make up for the cost. But if the user experience is degraded by our computations, we need to consider ways of streamlining our code. Maybe a quest for more-efficient algorithms will pay dividends, or maybe our algorithms are already efficient and there is no way to make them faster. Either way, we shouldn’t perform the computations at all if we know their output will be unchanged. In such a case, React provides the useMemo
hook to help us avoid unnecessary and wasteful work.
We start this chapter by being willfully wasteful, running the risk of crashing the browser with some gratuitously resource-intensive anagram generation. We call on useMemo
to protect the user from some seriously sluggish UI updates. We then bring the bookings to life in our example app, this time calling useMemo
to avoid regenerating grids of booking slots for no reason. When fetching the bookings for the selected week and bookable, we examine a method for coping with multiple requests and responses from within a useEffect
call.
The title of section 7.1 is a little messed up; let’s find out what it’s trying to teach us about React Hooks.
Say you’re trying to develop an anagram app that will find amusing anagrams of words, names, and phrases. It’s early in the development process and, so far, you have an app that finds all of the combinations of letters in some source text. In figure 7.1, your fledgling app is displaying the 12 distinct anagrams of the source text ball
. The app is live on CodeSandbox (https://codesandbox.io/s/anagrams-djwuy).
You can toggle between All anagrams and Distinct anagrams. For example, because “ball” has a repeated letter “l,” you could swap their positions and still have the word “ball.” The two identical words are counted separately in the All category but not in the Distinct category. You can also hide the generated anagrams, letting the app find new anagrams behind the scenes as you enter the source text, without having to render the new anagrams as you type.
Be careful! The number of anagrams shoots up as the number of letters in the source text increases. There are n! (n factorial) combinations of n letters. For four letters, that’s 4 × 3 × 2 × 1 = 24 combinations. For ten letters, there are 10!, or 3,628,800, combinations, as shown in figure 7.2. The app is limited to ten characters—remove the cap at your own risk!
A coworker provides you with the code for finding the anagrams. The algorithm is shown in the following listing. It could certainly be improved. But whatever the algorithm, you want to be performing such expensive calculations only if absolutely necessary.
Live: https://djwuy.csb.app/, Code: https://codesandbox.io/s/anagrams-djwuy
export function getAnagrams(source) { ❶ if (source.length < 2) { return [...source]; } const anagrams = []; const letters = [...source]; letters.forEach((letter, i) => { const without = [...letters]; without.splice(i, 1); getAnagrams(without).forEach(anagram => { ❷ anagrams.push(letter + anagram); }); }); return anagrams; } export function getDistinct(anagrams) { ❸ return [...new Set(anagrams)]; }
❶ Create a function to find all combinations of letters in some source text.
❷ Call the function recursively on source text with one letter removed.
❸ Create a function to remove duplicates from an array.
The algorithm takes each letter in a word and appends all the anagrams of the remaining letters. So, for “ball” it would find the following:
The main app calls getAnagrams
and getDistinct
to get the info it needs to display. The following listing is an earlier implementation. Can you spot any problems?
import React, { useState } from "react"; import "./styles.css"; import { getAnagrams, getDistinct } from "./anagrams"; ❶ export default function App() { const [sourceText, setSourceText] = useState("ball"); ❷ const [useDistinct, setUseDistinct] = useState(false); ❸ const [showAnagrams, setShowAnagrams] = useState(false); ❸ const anagrams = getAnagrams(sourceText); ❹ const distinct = getDistinct(anagrams); ❹ return ( <div className="App"> <h1>Anagrams</h1> <label htmlFor="txtPhrase">Enter some text...</label> <input type="text" value={sourceText} onChange={e => setSourceText(e.target.value.slice(0, 10))} ❺ /> <div className="count"> ❻ {useDistinct ? ( ❻ <p> ❻ There are {distinct.length} distinct anagrams. ❻ </p> ❻ ) : ( ❻ <p> ❻ There are {anagrams.length} anagrams of "{sourceText}". ❻ </p> ❻ )} ❻ </div> ❻ <p> <label> <input type="checkbox" checked={useDistinct} onClick={() => setUseDistinct(s => !s)} /> Distinct </label> </p> <p> <label> <input type="checkbox" checked={showAnagrams} onChange={() => setShowAnagrams(s => !s)} /> Show </label> </p> {showAnagrams && ( ❼ <p className="anagrams"> ❼ {distinct.map(a => ( ❼ <span key={a}>{a}</span> ❼ ))} ❼ </p> ❼ )} ❼ </div> ); }
❶ Import the anagram finder functions.
❷ Manage the source text state.
❸ Include flags for toggling distinct anagrams and anagram display.
❹ Use the anagram functions to generate the data.
❻ Display the number of anagrams.
❼ Display the list of anagrams.
The key problem is that the code calls the expensive anagram functions on every render. But the anagrams change only if the source text changes. You really shouldn’t generate the anagrams again if the user clicks either of the check boxes, toggling between All and Distinct anagrams, or showing and hiding the list. Here are the current calls to the anagram functions:
export default function App() { // variables const anagrams = getAnagrams(sourceText); ❶ const distinct = getDistinct(anagrams); ❶ return ( /* UI */ ) }
❶ The expensive functions run on every render.
We need a way of asking React to run the expensive functions only if their output is likely to be different. For getAnagrams
, that’s if the sourceText
value changes. For getDistinct
, that’s if the anagrams
array changes.
The following listing shows the code for the live example. It wraps the expensive functions in calls to the useMemo
hook, providing an array of dependencies for each call.
Live: https://djwuy.csb.app/, Code: https://codesandbox.io/s/anagrams-djwuy
import React, {useState, useMemo} from "react"; ❶ import "./styles.css"; import {getAnagrams, getDistinct} from "./anagrams"; export default function App() { const [sourceText, setSourceText] = useState("ball"); const [useDistinct, setUseDistinct] = useState(false); const [showAnagrams, setShowAnagrams] = useState(false); const anagrams = useMemo( ❷ () => getAnagrams(sourceText), ❸ [sourceText] ❹ ); const distinct = useMemo( ❺ () => getDistinct(anagrams), ❻ [anagrams] ❼ ); return ( /* UI */ ) }
❸ Pass the expensive function to useMemo.
❹ Specify a list of dependencies.
❺ Assign the value getDistinct returns to a variable.
❻ Wrap the call to getDistinct in another function.
❼ Rerun the getDistinct function only when the anagrams array changes.
In this version, React should call getAnagrams
only when sourceText
changes, and should call getDistinct
only when anagrams
changes. Users can toggle at will without causing a cascade of costly calls as the app tries to keep up while rebuilding the same million anagrams again and again.
You could see the last example, decide there’s no more to learn, and bury your head in the sand—some emu. Or be too timid to ask for more details—Mouse? Me? But, be brave, lean on React, and calm those costly calls—useMemo!
If we have a function, expensiveFn
, that takes time and resources to calculate its return value, then we want to call the function only when absolutely necessary. By calling the function inside the useMemo
hook, we ask React to store a value computed by the function for a given set of arguments. If we call the function inside useMemo
again, using the same arguments as the previous call, it should return the stored value. If we pass different arguments, it will use the function to compute a new value and update its store before returning the new value. The process of storing a result for a given set of arguments is called memoizing.
When calling useMemo
, pass it a create function and a list of dependencies, as shown in figure 7.3.
The list of dependencies is an array of values and should include all the values the function uses in its computation. On each call, useMemo
compares the dependency list to the previous list. If each list holds the same values in the same order, useMemo
may return the stored value. If any value in the list has changed, useMemo
will call the function and store and return the function’s return value. To reiterate, useMemo
may return the stored value. React reserves the right to clear its store if it needs to free up memory. So, it might call the expensive function even if the dependencies are unchanged.
If you omit the dependency list, useMemo
always runs your function, which kind of defeats the purpose! If you pass an empty array, the values in the list never change, so useMemo
could always return the stored value. It may, however, decide to clear its store and run your function again anyway. It’s almost certainly best to avoid that kind of maybe-or-maybe-not behavior.
That’s how useMemo
works. We see it in action again in the bookings example app in section 7.4, memoizing a function for generating a grid of booking slots. First, we use our state-sharing and React Hooks skills to put the Bookings page components into place and pass them the bits and pieces they need to work nicely together.
So far, the Bookables and Users pages have had all the attention in the bookings app; it’s about time the Bookings page got some love! We need to put the shared-state concepts from chapter 6 into action and decide which components will manage which state as we let users view bookings for different bookables and different weeks.
Figure 7.4 shows the layout of the Bookings page, with the list of bookables on the left and the bookings info taking up the rest of the page. We have a BookingsPage
component for the page itself, a BookablesList
component for the list on the left, and a Bookings
component for the rest of the page. The bookings info includes a week picker, an area to display a bookings grid, and an area to display the details of a selected booking.
Figure 7.4 has placeholders for the bookings grid and the booking details. We’ll bring the bookings grid to life and incorporate the useMemo
hook in section 7.4. We’ll populate the booking details and introduce the useContext
hook in chapter 8. In this section, we put the pieces into place on the page.
This book uses the bookings app to teach you about React Hooks. To save you time and effort, I'm focusing more on teaching hooks than I am on teaching you how to code the bookings app, which could get very repetitive and wouldn't benefit learning React. So, sometimes, the book sets challenges and points you to the example’s GitHub repo to get the latest code for certain components. With the Bookings page, the example app is edging into complexity, so a few more cases of changes in the repo are not fully listed in the book; I’ll make it clear when you need to check the repo.
Table 7.1 lists the components in play for the Bookings page, along with their main function and the shared state they manage. In chapter 8, we’ll use the useContext
hook to access the current user from the BookingDetails
component; although we don’t work with the App
component in this chapter, it’s included in the table so you can see the full hierarchy of components.
We’ll work from the BookingsPage
down; the listings should give you a good sense of the structure of the page and of the flow of state through the hierarchy of components. The discussion is split into two subsections, using the shared state as the focus:
All the pieces shown in table 7.1 will need to be in position before the app returns to a working state, but the listings aren’t long, so we’ll get there soon.
Our first piece of shared state is the selected bookable. It’s used by the BookablesList
and Bookings
components. (Remember, the Bookings
component is the container for the WeekPicker
, BookingsGrid
, and BookingDetails
components.) Their nearest shared parent is the Bookings page itself.
Listing 7.4 shows the BookingsPage
component calling useState
to manage the selected bookable. BookingsPage
also passes the updater function, setBookable
, to BookablesList
so that users can choose a bookable from the list. It no longer directly imports WeekPicker
.
Branch: 0701-bookings-page, File: src/components/Bookings/BookingsPage.js
import {useState} from "react"; import BookablesList from "../Bookables/BookablesList"; import Bookings from "./Bookings"; export default function BookingsPage () { const [bookable, setBookable] = useState(null); ❶ return ( <main className="bookings-page"> <BookablesList bookable={bookable} ❷ setBookable={setBookable} ❸ /> <Bookings bookable={bookable} ❹ /> </main> ); }
❶ Manage the selected bookable with the useState hook.
❷ Pass the bookable down so it can be highlighted in the list.
❸ Pass the updater function so users can select a bookable.
❹ Let the Bookings component display the bookings for the selected bookable.
The page passes the selected bookable to the Bookings
component (created next) so that it can show the bookable’s bookings. To show the correct bookings (and to let users make new bookings), the Bookings
component also needs to know the selected week. Let’s see how it manages that state itself.
Users can switch weeks by using the week picker. They can navigate forward a week or back a week and jump straight to the week containing today’s date. They can also enter a date into a text box and go to the week for that date. To share the selected date with the bookings grid, we lift the week picker’s reducer up into the Bookings
component, as shown in the following listing.
Branch: 0701-bookings-page, File: src/components/Bookings/Bookings.js
import {useState, useReducer} from "react"; import {getWeek} from "../../utils/date-wrangler"; import WeekPicker from "./WeekPicker"; import BookingsGrid from "./BookingsGrid"; import BookingDetails from "./BookingDetails"; import weekReducer from "./weekReducer"; ❶ export default function Bookings ({bookable}) { ❷ const [week, dispatch] = useReducer( ❸ weekReducer, new Date(), getWeek ); const [booking, setBooking] = useState(null); ❹ return ( <div className="bookings"> <div> <WeekPicker dispatch={dispatch} /> <BookingsGrid week={week} bookable={bookable} booking={booking} setBooking={setBooking} /> </div> <BookingDetails booking={booking} bookable={bookable} /> </div> ); }
❶ Import the existing reducer for the week picker.
❷ Destructure the current bookable from props.
❸ Manage the shared state for the selected week.
❹ Manage the shared state for the selected booking.
The Bookings
component imports the reducer and passes it in when calling the useReducer
hook. It also calls the useState
hook to manage the shared selected booking state for both the BookingsGrid
and BookingDetails
components.
Update the WeekPicker
component so that it receives dispatch
as a prop, no longer calling useReducer
itself. It doesn’t need to display the selected date, so remove that from the end of its returned UI, and remove any redundant imports. Check the repo for the latest version (src/components/Bookings/WeekPicker.js).
In section 7.4, we build up the bookings grid to show actual bookings. For the current repo branch, let’s just add a couple of placeholder components to check that the page structure is working nicely. The following listing shows our temporary bookings grid.
Branch: 0701-bookings-page, File: src/components/Bookings/BookingsGrid.js
export default function BookingsGrid (props) { const {week, bookable, booking, setBooking} = props; return ( <div className="bookings-grid placeholder"> <h3>Bookings Grid</h3> <p>{bookable?.title}</p> <p>{week.date.toISOString()}</p> </div> ); }
The following listing shows our temporary details component.
Branch: 0701-bookings-page, File: src/components/Bookings/BookingDetails.js
export default function BookingDetails () { return ( <div className="booking-details placeholder"> <h3>Booking Details</h3> </div> ); }
Everything should now be in place, and the app should be back in working order. The Bookings page should look like figure 7.4 (if you have the latest CSS, or roll your own for the placeholders).
Make a small change to BookablesList
, removing the code for moving focus to the Next button. This will just slim down the component for future changes. The updates are on the current branch: /src/components/Bookables/BookablesList.js.
With all the components in place and a sense of where the page manages each piece of shared state, it’s time to introduce a new React Hook to the bookings app. The useMemo
hook will help us run expensive calculations only when necessary. Let’s see why we need it and how it helps.
With the Bookings page structure and hierarchy in place, we’re ready to build up our most complicated component yet, the BookingsGrid
. In this section, we develop the grid so that it can display booking slots for a bookable in a given week and place any existing bookings in the grid. Figure 7.5 shows the grid with three rows for sessions and five columns for dates. Four existing bookings are in the grid, and the user has selected one of the bookings.
We develop the component in five stages:
Generating a grid of sessions and dates—we want to transform our data to make looking up empty booking slots easier.
Generating a lookup for bookings—we want to transform our data to make looking up existing bookings easier.
Providing a getBookings
data-loading function—it will handle building the query strings for our request to the JSON server.
Creating the BookingsGrid
component—this is the meat of the section and is where we enlist the help of useMemo
.
Coping with racing responses when fetching data in useEffect
.
In stage 5, we see how to manage multiple requests and responses for data within calls to the useEffect
hook, with later requests superseding earlier ones, and how to manage errors. There’s a lot to sink our teeth into, so let’s get started by transforming lists of days and sessions into two-dimensional booking grids.
The bookings grid displays empty booking slots and existing bookings in a table, with sessions as rows and dates as columns. An example grid of booking slots for the Meeting Room bookable is shown in figure 7.6.
Users book different bookables for different sessions and days of the week. When the user chooses a new bookable, the BookingsGrid
component needs to generate a new grid, for the latest sessions and dates. Figure 7.7 shows the grid generated when the user switches to the Lounge bookable.
Each cell in the grid corresponds to a booking slot. We want the grid data to be structured so that it’s easy to access the data for a specific booking slot. For example, to access the data for the Breakfast session on August 3, 2020, we use this:
grid["Breakfast"]["2020-08-03"]
For an empty booking slot, the booking data looks like this:
{ "session": "Breakfast", "date": "2020-08-03", "bookableId": 4, "title": "" }
In the data from the database, each bookable specifies the sessions and days for which it can be booked. Here’s the data for the Meeting Room:
"id": 1, "group": "Rooms", "title": "Meeting Room", "notes": "The one with the big table and interactive screen.", "sessions": [1, 2, 3], "days": [1, 2, 3, 4, 5]
The days
represent days in the week, where Sunday = 0, Monday = 1, . . . , Saturday = 6. So, the Meeting Room can be booked for sessions 1, 2, and 3, Monday through Friday, as we saw in figure 7.6. To get the specific dates for the bookings, rather than just the day numbers, we also need the start date for the week we want to display. And to get the specific session names, we need to import the array of session names from the config file, static.json.
The grid generator function, getGrid
, is in the following listing. The calling code passes getGrid
the current bookable and the start date for the selected week.
Branch: 0702-bookings-memo, File: /src/components/Bookings/grid-builder.js
import {sessions as sessionNames} from "../../static.json"; ❶ import {addDays, shortISO} from "../../utils/date-wrangler"; export function getGrid (bookable, startDate) { ❷ const dates = bookable.days.sort().map( ❸ d => shortISO(addDays(startDate, d)) ❸ ); ❸ const sessions = bookable.sessions.map(i => sessionNames[i]); ❹ const grid = {}; sessions.forEach(session => { grid[session] = {}; ❺ dates.forEach(date => grid[session][date] = { ❻ session, date, bookableId: bookable.id, title: "" }); }); return { ❼ grid, ❼ dates, ❼ sessions ❼ }; ❼ }
❶ Assign the session names to the sessionNames variable.
❷ Accept the current bookable and week start date as arguments.
❸ Use the day numbers and start date to create an array of dates for the week.
❹ Use the session names and numbers to create an array of session names.
❺ Assign an object to grid for each session.
❻ Assign a booking object for each date to each session.
❼ In addition to the grid, return the dates and sessions arrays for convenience.
The getGrid
function starts by mapping the day and session indexes to dates and session names. It uses a truncated ISO 8601 format for dates:
const dates = bookable.days.sort().map( d => shortISO(addDays(startDate, d)) );
The shortISO
function has been added to the utils/date-wrangler.js file that also contains the addDays
function. shortISO
returns the date part of the ISO-string for a given date:
export function shortISO (date) { return date.toISOString().split("T")[0]; }
For example, for a JavaScript date object representing August 3, 2020, shortISO
returns the string "2020-08-03"
.
The code in the listing also imports the session names from static.json and assigns them to the sessionNames
variable. The session data looks like this:
"sessions": [ "Breakfast", "Morning", "Lunch", "Afternoon", "Evening" ]
Each session index from the bookable is mapped to its session name:
const sessions = bookable.sessions.map(i => sessionNames[i]);
So, if the selected bookable is the Meeting Room, then bookable.sessions
is the array [1,
2,
3]
and sessions
becomes ["Morning",
"Lunch",
"Afternoon"]
.
Having acquired the dates and session names, getGrid
then uses nested forEach
loops to build up the grid of booking sessions. You could use the reduce
array method here, but I find the forEach
syntax easier to follow in this case. (Don’t worry, reduce
fans; the next listing employs its services.)
We also want an easy way to look up existing bookings. Figure 7.8 shows a bookings grid with existing bookings in four cells.
We want to use the session name and date to access the data for an existing booking, like this:
bookings["Morning"]["2020-06-24"]
The lookup expression should return the data for the Movie Pitch! booking, with this structure:
{ "id": 1, "session": "Morning", "date": "2020-06-24", "title": "Movie Pitch!", "bookableId": 1, "bookerId": 2 }
But the server returns the bookings data as an array. We need to transform the array of bookings into the handy lookup object. Listing 7.9 adds a new function, transformBookings
, to the grid-builder.js file from listing 7.8.
Branch: 0702-bookings-memo, File: /src/components/Bookings/grid-builder.js
export function transformBookings (bookingsArray) { return bookingsArray.reduce((bookings, booking) => { ❶ const {session, date} = booking; ❷ if (!bookings[session]) { ❸ bookings[session] = {}; ❸ } ❸ bookings[session][date] = booking; ❹ return bookings; }, {}); ❺ }
❶ Use reduce to step through each booking and build up the bookings lookup.
❷ Destructure the session and date for the current booking.
❸ Add a property to the lookup for each new session.
❹ Assign the booking to its session and date.
❺ Start the bookings lookup as an empty object.
The transformBookings
function uses the reduce
method to step through each booking in the array and build up the bookings
lookup object, assigning the current booking to its allotted lookup slot. The lookup object that transformBookings
creates has entries for only the existing bookings, not necessarily for every cell in the bookings grid.
We now have functions to generate the grid and transform an array of bookings into a lookup object. But where are the bookings?
The BookingsGrid
component needs some bookings to display for the selected bookable and week. We could use our existing getData
function from within an effect in the BookingsGrid
component, building up the necessary URL there. Instead, let’s keep our data-access functions in the api.js file. The following listing shows the part of the updated file with our new getBookings
function.
Branch: 0702-bookings-memo, File: /src/utils/api.js
import {shortISO} from "./date-wrangler"; ❶ export function getBookings (bookableId, startDate, endDate) { ❷ const start = shortISO(startDate); ❸ const end = shortISO(endDate); ❸ const urlRoot = "http://localhost:3001/bookings"; const query = `bookableId=${bookableId}` + ❹ `&date_gte=${start}&date_lte=${end}`; ❹ return getData(`${urlRoot}?${query}`); ❺ }
❶ Import a function to format dates.
❷ Export the new getBookings function.
❸ Format the dates for the query string.
❺ Fetch the bookings, returning a promise.
The getBookings
function accepts three arguments: bookableId
, startDate
, and endDate
. It uses the arguments to build up the query string for the required bookings. For example, to fetch the bookings for the Meeting Room between Sunday, June 21, 2020, and Saturday, June 27, 2020, the query string is as follows:
bookableId=1&date_gte=2020-06-21&date_lte=2020-06-27
The json-server
we have running will parse the query string and return the requested bookings as an array, ready for transformation into a lookup object.
With the helper functions in place, it’s time to put them to good use as we construct the BookingsGrid
component.
For a given bookable and week, the BookingsGrid
component fetches the bookings and displays them, highlighting any selected booking. It uses three React Hooks: useState
, useEffect
, and useMemo
. We break the code for the component across a number of listings, in this subsection and the next, starting with the imports and component skeleton in the following listing.
Branch: 0702-bookings-memo, File: /src/components/Bookings/BookingsGrid.js
import {useEffect, useMemo, useState, Fragment} from "react"; ❶ import {getGrid, transformBookings} from "./grid-builder"; ❷ import {getBookings} from "../../utils/api"; ❸ import Spinner from "../UI/Spinner"; export default function BookingsGrid () { // 1. Variables // 2. Effects // 3. UI helper // 4. UI }
❶ Import useMemo to memoize the grid.
❷ Import the new grid functions.
❸ Import a new data-loading function.
The code imports the helper functions created previously and the three hooks. As you’ll see over the next few listings, we use the useState
hook to manage the state for the bookings and any errors, the useEffect
hook to fetch the bookings data from the server, and the useMemo
hook to reduce the number of times we generate the grid data.
The Bookings
component passes the BookingsGrid
component the selected bookable, the selected week, and the currently selected booking along with its updater function, as highlighted in the following listing.
Branch: 0702-bookings-memo, File: /src/components/Bookings/BookingsGrid.js
export default function BookingsGrid ( {week, bookable, booking, setBooking} ❶ ) { const [bookings, setBookings] = useState(null); ❷ const [error, setError] = useState(false); ❸ const {grid, sessions, dates} = useMemo( ❹ () => bookable ? getGrid(bookable, week.start) : {}, ❺ [bookable, week.start] ❻ ); // 2. Effects // 3. UI helper // 4. UI }
❷ Handle the bookings data locally.
❸ Handle loading errors locally.
❹ Wrap the grid generator function with useMemo.
❺ Call the grid generator only if there’s a bookable.
❻ Regenerate the grid when the bookable or week changes.
BookingsGrid
handles the bookings and error state itself with two calls to the useState
hook. It then uses the getGrid
function from section 7.4.2 to generate the grid, assigning the returned grid, sessions, and dates data to local variables. We’ve decided to see getGrid
as an expensive function, wrapping it with useMemo
. Why might it warrant such treatment?
When the user chooses a bookable on the Bookings page, the Bookings
component displays a grid of booking slots for the bookable’s available sessions and dates. It generates the data for the grid based on the bookable’s properties and the selected week. As we’ll see in the next listing, the BookingsGrid
component uses the fetch-on-render, data-loading strategy, sending a request for data after the initial render. The grid, shown in figure 7.9, displays a loading indicator in the top-left cell and reduces the opacity of the body cells until the data arrives.
When the data arrives, the grid re-renders, hiding the loading indicator and showing the bookings for the selected week. Figure 7.10 shows four bookings in the grid.
With the bookings in place, the user is now free to select an existing booking or an empty booking slot. In figure 7.11, the user has selected the Movie Pitch! booking and, yet again, the component has re-rendered, highlighting the cell.
The component renders for each change in status, as listed in table 7.2, although the underlying grid data for the booking slots hasn’t changed.
For the events listed, we don’t want to regenerate the underlying grid data on each re-render, so we use the useMemo
hook, specifying the bookable and start date for the week as dependencies:
const {grid, sessions, dates} = useMemo( () => bookable ? getGrid(bookable, week.start) : {}, [bookable, week.start] );
By wrapping getGrid
in useMemo
, we ask React to store the generated grid lookup and to call getGrid
again only if the bookable or start date changes. For the three re-rendering scenarios in table 7.2 (not for the initial render), React should return the stored grid, avoiding unnecessary computation.
In reality, for the size of grids we’re generating, we don’t really need useMemo
. Modern browsers, JavaScript, and React will hardly notice the work required. There’s also some overhead in requiring React to store functions, return values, and dependency values, so we don’t want to memoize everything. As we saw with the anagrams example earlier in the chapter, however, sometimes expensive functions can adversely affect performance, so it’s good to have the useMemo
hook in your toolbelt.
Although the main focus of this chapter is the useMemo
hook, a useful technique for data-fetching within a call to useEffect
is worth flagging with a subsection heading. Let’s see how to avoid getting multiple requests and responses knotted.
When interacting with the bookings app, the user might get a little click-happy and switch quickly between bookables and weeks, initiating a flurry of data requests. We want to display the data for only their last selection. Unfortunately, we’re not in control of when the data returns from the server, and an older request might resolve after a more recent one, leaving the display out of sync with the user’s selection.
We could try to implement a way to cancel in-flight requests. If the data response isn’t too large, however, it’s easier to simply let the requests run their course and ignore the unwanted data when it arrives. In this subsection, we finish off the BookingsGrid
component, fetching the bookings data, and building the UI for display.
The BookingsGrid
component loads the bookings for the selected bookable and week. Listing 7.13 shows calls to our helper functions, getBookings
and transformBookings
, wrapped inside a call to useEffect
. The effect runs whenever the week or bookable changes.
Branch: 0702-bookings-memo, File: /src/components/Bookings/BookingsGrid.js
export default function BookingsGrid ( {week, bookable, booking, setBooking} ) { // 1. Variables useEffect(() => { if (bookable) { let doUpdate = true; ❶ setBookings(null); setError(false); setBooking(null); getBookings(bookable.id, week.start, week.end) ❷ .then(resp => { if (doUpdate) { ❸ setBookings(transformBookings(resp)); ❹ } }) .catch(setError); return () => doUpdate = false; ❺ } }, [week, bookable, setBooking]); ❻ // 3. UI helper // 4. UI }
❶ Use a variable to track whether the bookings data is current.
❷ Call our getBookings data-fetching function.
❸ Check if the bookings data is current.
❹ Create a bookings lookup and assign it to state.
❺ Return a cleanup function to invalidate the data.
❻ Run the effect when the bookable or week changes.
The code uses a doUpdate
variable to match each request with its data. The variable is initially set to true
:
let doUpdate = true;
For a particular request, the callback function in the then
clause will update the state only if doUpdate
is still true
:
if (doUpdate) { setBookings(transformBookings(resp)); }
When the user selects a new bookable or switches to a new week, React reruns the component, and the effect runs again to load the newly selected data. The in-flight data from the previous request is no longer needed. Before rerunning an effect, React calls any associated cleanup function for the previous invocation of the effect. Our effect uses the cleanup function to invalidate the in-flight data:
return () => doUpdate = false;
When the previously requested bookings arrive, the then
clause from the associated call to getBookings
will see the data is stale and won’t update the state.
If the bookings are current, the then
clause transforms the linear array of bookings into a lookup structure by passing the response to the transformBookings
function. The lookup object is assigned to local state with setBookings
.
The contents and behavior of a cell in the bookings grid depend on whether there are any bookings to display and whether the user has selected the cell. Figure 7.12 shows a couple of empty cells and a cell for an existing booking, Movie Pitch!
When a user selects a cell, that cell should be highlighted, whether the cell shows an existing booking or an empty booking slot. Figure 7.13 shows the grid after the user has selected the Movie Pitch! booking. CSS styles and the cell’s class
attribute are used to change the cell’s appearance.
Listing 7.14 has the code for a cell
helper function that returns the UI for a single cell in the bookings grid. It uses the two lookup objects, bookings
and grid
, to get the data for the cell, set the cell’s class, and attach an event handler if there are bookings. The cell
function is in the scope of BookingsGrid
and can access the booking
, bookings
, grid
, and setBookings
variables.
Branch: 0702-bookings-memo, File: /src/components/Bookings/BookingsGrid.js
export default function BookingsGrid ( {week, bookable, booking, setBooking} ) { // 1. Variables // 2. Effects function cell (session, date) { const cellData = bookings?.[session]?.[date] ❶ || grid[session][date]; ❶ const isSelected = booking?.session === session ❷ && booking?.date === date; ❷ return ( <td key={date} className={isSelected ? "selected" : null} onClick={bookings ? () => setBooking(cellData) : null} ❸ > {cellData.title} </td> ); } // 4. UI }
❶ First check the bookings lookup, then the grid lookup.
❷ Use optional chaining because there might not be a booking.
❸ Set a handler only if bookings have been loaded.
The data for a cell comes either from the existing bookings in the bookings
lookup or from the empty booking slot data in the grid
lookup. The code uses optional chaining syntax with square-bracket notation to assign the correct value to the cellData
variable:
const cellData = bookings?.[session]?.[date] || grid[session][date];
The bookings
lookup has data for only the existing bookings, but the grid
lookup has data for every session and date. We need the optional chaining for bookings
but not for grid
.
We set the click handler on the cell only if there are bookings. While bookings are loading, when a user switches bookables or weeks, the handler is set to null
and the user can’t interact with the grid.
The final piece of the BookingsGrid
puzzle returns the UI. As ever, the UI is driven by the state. We check whether the grid of booking slots has been generated, whether the bookings have been loaded, and whether there is an error. We then return either alternative UI (loading text) or additional UI (an error message), or we set class names to show, hide, or highlight elements. Figure 7.14 shows the bookings grid for three states:
There are no bookings. The grid shows a loading indicator. The grid is inactive, and the user can’t interact with the grid.
The bookings have loaded. The grid hides the loading indicator. The grid is active, and the user can interact with the grid.
The bookings have loaded. The grid hides the loading indicator. The grid is active, and the user has selected a cell.
In figure 7.15, you can see an error displayed right above the date headings for the grid.
The following listing shows the error section, uses class names to control whether the grid is active, and calls our UI helper function, cell
, to get the UI for each table cell.
Branch: 0702-bookings-memo, File: /src/components/Bookings/BookingsGrid.js
export default function BookingsGrid ( {week, bookable, booking, handleBooking} ) { // 1. Variables // 2. Effects // 3. UI helper if (!grid) { return <p>Loading...</p> } return ( <Fragment> {error && ( <p className="bookingsError"> ❶ {`There was a problem loading the bookings data (${error})`} ❶ </p> ❶ )} <table className={bookings ? "bookingsGrid active" : "bookingsGrid"} ❷ > <thead> <tr> <th> <span className="status"> <Spinner/> ❸ </span> </th> {dates.map(d => ( <th key={d}> {(new Date(d)).toDateString()} </th> ))} </tr> </thead> <tbody> {sessions.map(session => ( <tr key={session}> <th>{session}</th> {dates.map(date => cell(session, date))} ❹ </tr> ))} </tbody> </table> </Fragment> ); }
❶ Show an error section at the top of the grid if there’s an error.
❷ Include an “active” class when the bookings data has loaded.
❸ Include a loading indicator in the top-left cell.
❹ Use the UI helper function to generate each table cell.
If bookings
is not null
, a class of active
is assigned to the table. The CSS for the app hides the loading indicator and sets the cell opacity to 1 when the grid is active.
In the code, we inspect the state ourselves and decide what UI to return from within the component. It’s also possible to use React error boundaries to specify error UI and React’s Suspense
component to specify fallback UI while data is loading, separately from individual components. We use error boundaries to catch errors and Suspense
components to catch promises (loading data) in part 2.
Before that, we need to create our BookingDetails
component to show the details of whichever booking slot or existing booking a user clicks. The new component needs access to the current user of the app, stored all the way up in the root component, App
. Rather than drilling the user value down through multiple layers of component props, we’ll enlist the help of React’s Context API and the useContext
hook.
Try to avoid unnecessarily rerunning expensive computations by wrapping them in the useMemo
hook.
Pass useMemo
the expensive function you want to memoize:
const value = useMemo( () => expensiveFn(dep1, dep2), [dep1, dep2] );
Pass the useMemo
hook a list of dependencies for the expensive function:
const value = useMemo( () => expensiveFn(dep1, dep2), [dep1, dep2] );
If the values in the dependency array don’t change from one call to the next, useMemo
can return its stored result for the expensive function.
Don’t rely on useMemo
to always use a memoized value. React may discard stored results if it needs to free up memory.
Use JavaScript’s optional chaining syntax with square brackets to access properties of variables that may be undefined
. Include a period, even when working with square brackets:
const cellData = bookings?.[session]?.[date]
When fetching data within a call to useEffect
, combine a local variable and the cleanup function to match a data request with its response:
useEffect(() => { let doUpdate = true; fetch(url).then(resp => { if (doUpdate) { // perform update with resp } }); return () => doUpdate = false; }, [url]);
If the component re-renders with a new url
, the cleanup function for the previous render will set the previous render’s doUpdate
variable to false
, preventing the previous then
method callback from performing updates with stale data.
18.222.179.186