7 Managing performance with useMemo

This chapter covers

  • Using the useMemo hook to avoid rerunning expensive computations
  • Controlling useMemo with a dependency array
  • Considering the user experience as your app re-renders
  • Handling race conditions when fetching data
  • Using JavaScript’s optional chaining syntax with square brackets

React 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.

7.1 Breaking the cook’s heart by calling, “O, shortcake!”

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).

Figure 7.1 The Anagrams app counts and displays anagrams of text entered by the user. The user can count all anagrams or only distinct anagrams, and can toggle the display of the anagrams.

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!

Figure 7.2 Be careful! The number of anagrams increases quickly as the source text gets longer. There are over 3.5 million anagrams of a 10-letter word.

7.1.1 Generating anagrams with an expensive algorithm

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

Listing 7.1 Finding anagrams

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:

“b” + anagrams of “all”

“a” + anagrams of “bll”

“l” + anagrams of “bal”

“l” + anagrams of “bal”

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?

Listing 7.2 The anagrams app before the fix

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.

Cap the number of letters.

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.

7.1.2 Avoiding redundant function calls

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

Listing 7.3 The anagrams app with useMemo

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 */ )
}

Import the useMemo hook.

Call useMemo.

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!

7.2 Memoizing expensive function calls with 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.

Figure 7.3 Call the useMemo hook with a function and a list of dependencies.

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.

7.3 Organizing the components on the Bookings page

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.

Figure 7.4 The Bookings page includes two components: one for the list of bookables, and one containing the week picker, bookings grid, and booking details.

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.

Table 7.1 Components for the Bookings page

Component

Role

Managed state

Hook

App

Render header with links to pages. Render user picker. Use routes to render correct page.

Current user

useState + Context API—see chapter 8

BookingsPage

Render BookablesList and Bookings components.

Selected bookable

useState

BookablesList

Render list of bookables and let users select a bookable.

 

 

Bookings

Render WeekPicker, BookingsGrid, and BookingDetails components.

Selected week and selected booking

useReducer and useState

WeekPicker

Let users switch between weeks to view.

 

 

BookingsGrid

Display a grid of booking slots for the selected bookable and week. Populate the grid with any existing bookings. Highlight the selected booking.

 

 

BookingDetails

Display details of the selected booking.

 

 

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:

  • Managing the selected bookable with useState

  • Managing the selected week and booking with useReducer and useState

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.

7.3.1 Managing the selected bookable with useState

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

Listing 7.4 The BookingsPage component

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.

7.3.2 Managing the selected week and booking with useReducer and useState

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

Listing 7.5 The Bookings component

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.

Challenge 7.1

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

Listing 7.6 The BookingsGrid placeholder

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

Listing 7.7 The BookingDetails placeholder

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).

Challenge 7.2

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.

7.4 Efficiently building the bookings grid with useMemo

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.

Figure 7.5 The bookings grid showing bookings for the selected bookable and week. A booking in the grid has been selected.

We develop the component in five stages:

  1. Generating a grid of sessions and dates—we want to transform our data to make looking up empty booking slots easier.

  2. Generating a lookup for bookings—we want to transform our data to make looking up existing bookings easier.

  3. Providing a getBookings data-loading function—it will handle building the query strings for our request to the JSON server.

  4. Creating the BookingsGrid component—this is the meat of the section and is where we enlist the help of useMemo.

  5. 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.

7.4.1 Generating a grid of sessions and dates

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.

Figure 7.6 The bookings grid for the Meeting Room bookable. It has rows for each session and columns for each date.

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.

Figure 7.7 The bookings grid for the Lounge bookable. The Lounge is available for five sessions on every day of the week.

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

Listing 7.8 The grid generator

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.)

7.4.2 Generating a lookup for bookings

We also want an easy way to look up existing bookings. Figure 7.8 shows a bookings grid with existing bookings in four cells.

Figure 7.8 The 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

Listing 7.9 The transformBookings function

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?

7.4.3 Providing a getBookings data-loading function

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

Listing 7.10 The getBookings API function

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.

Build up 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.

7.4.4 Creating the BookingsGrid component and calling useMemo

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

Listing 7.11 The BookingsGrid component: Skeleton

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.

Variables

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

Listing 7.12 The BookingsGrid component: 1. Variables

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  
}

Destructure the props.

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.

Figure 7.9 The BookingsGrid component displays a loading spinner in its top-left cell and reduces the opacity of the grid cells while a fetch is in progress.

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.

Figure 7.10 The bookings grid showing four bookings

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.

Figure 7.11 The bookings grid showing a selected booking

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.

Table 7.2 Bookings grid rendering behavior for different events

Event

Render with

Initial render

Blank grid

Data fetching

Loading indicator

Data loaded

Bookings in cells

Booking selected

Highlighted selection

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.

7.4.5 Coping with racing responses when fetching data in useEffect

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.

Effects

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

Listing 7.13 The BookingsGrid component: 2. Effects

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.

UI helper function

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!

Figure 7.12 Cells in the grid represent existing bookings where they exist, or just the underlying grid data for session and date.

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.

Figure 7.13 The selected cell is displayed using different CSS styles.

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

Listing 7.14 The BookingsGrid component: 3. UI helper

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.

 

Figure 7.14 The display of a cell depends on whether the grid is active and whether a cell has been selected. While bookings are loading, the UI shows the loading indicator, and the grid is not active.

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.

UI

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:

  1. There are no bookings. The grid shows a loading indicator. The grid is inactive, and the user can’t interact with the grid.

  2. The bookings have loaded. The grid hides the loading indicator. The grid is active, and the user can interact with the grid.

  3. The bookings have loaded. The grid hides the loading indicator. The grid is active, and the user has selected a cell.

Figure 7.15 The BookingsGrid component displays any errors above the grid.

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

Listing 7.15 The BookingsGrid component: 4. UI

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.

Summary

  • 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.179.186