6 Managing application state

This chapter covers

  • Passing shared state to those components that need it
  • Coping when state isn’t passed down—the props are missing
  • Lifting state up the component tree to make it more widely available
  • Passing dispatch and updater functions to child components
  • Maintaining function identity with the useCallback hook

Up to this point, we’ve seen how components can manage their own state with the useState, useReducer, and useRef hooks and load state data with the useEffect hook. It’s common, however, for components to work together, using shared state values to generate their UI. Each component may have a whole hierarchy of descendant components nested within it, chirping and chirruping to be fed data, so state values may need to reach deep down into the descendant depths.

In this chapter, we investigate concepts and methods for deciding how to manage the availability of state values for child components that need to consume them, by lifting state to common parents. In chapter 8, we’ll see how and when React’s Context API can be used to make values available directly to components that need them. Here, we stick to using props to pass state down to children.

We start, in section 6.1, with a new Colors component that shares a selected color with three child components. We see how to update the shared state, managed by the parent, from a child. The rest of the chapter uses the bookings app example to explore two approaches to sharing state: passing a state object and a dispatch function for a reducer to the children and passing a single state value and its updater function to the children. Both approaches are common patterns and help to highlight some common questions regarding state, props, effects, and dependencies. We finish with a look at useCallback, a hook that lets us enlist React’s help to maintain the identity of functions we pass as props, particularly when child components treat those functions as dependencies.

For our first trick, let’s refresh our knowledge of props: pick a color, any color. . . .

6.1 Passing shared state to child components

When different components use the same data to build their UI, the most explicit way to share that data is to pass it as a prop from parent to children. This section introduces passing props (in particular, passing the state value and updater function returned by useState) by looking at a new example, a Colors component, shown in figure 6.1. The component includes three UI sections:

  • A list of colors with the selected color highlighted

  • Text showing the selected color

  • A bar with a background set to the selected color

Figure 6.1 The Colors component. When a user selects a color, the menu, text, and color bar all update. When goldenrod is selected, its menu circle is larger, the text says “. . . goldenrod!” and the bar’s color is goldenrod.

Clicking a color in the list (one of the circles) highlights that selection and updates the text and the color bar. You can see the component in action on CodeSandbox (https://hgt0x.csb.app/).

6.1.1 Passing state from a parent by setting props on the children

Listing 6.1 shows the code for the Colors component. It imports three child components: ColorPicker, ColorChoiceText, and ColorSample. Each child needs the selected color, so the Colors component holds that state and passes it to them as a prop, an attribute in the JSX. It also passes the available colors and the setColor updater function to the ColorPicker component.

Live: https://hgt0x.csb.app/, Code: https://codesandbox.io/s/colorpicker-hgt0x

Listing 6.1 The Colors component

import React, {useState} from "react";
 
import ColorPicker from "./ColorPicker";                                
import ColorChoiceText from "./ColorChoiceText";                        
import ColorSample from "./ColorSample";                                
 
export default function Colors () {
  const availableColors = ["skyblue", "goldenrod", "teal", "coral"];    
                                                                        
  const [color, setColor] = useState(availableColors[0]);               
 
  return (
    <div className="colors">
      <ColorPicker
        colors={availableColors}                                        
        color={color}                                                   
        setColor={setColor}                                             
      />
      <ColorChoiceText color={color} />                                 
      <ColorSample color={color} />                                     
    </div>
  );
}

Import the child components.

Define state values.

Pass the appropriate state values to the child components as props.

The Colors component passes down two types of props: state values to be used in the children’s UI, colors and color; and a function to update the shared state, setColor. Let’s look at state values first.

6.1.2 Receiving state from a parent as a prop

Both the ColorChoiceText component and the ColorSample component display the currently selected color. ColorChoiceText includes it in its message, and ColorSample uses it to set the background color. They receive the color value from the Colors component, as shown in figure 6.2.

Figure 6.2 The Colors component passes the current color state value to the child components.

Colors is the closest shared parent of the child components that share the state, so we manage the state within Colors. Figure 6.3 shows the ColorChoiceText component displaying a message that includes the selected color. The component simply uses the color value as part of its UI; it doesn’t need to update the value.

Figure 6.3 The ColorChoiceText component includes the selected color in its message.

The ColorChoiceText component’s code is in listing 6.2. When React calls the component, it passes it as the component’s first argument, an object containing all of the props set by the parent. The code here destructures the props, assigning the color prop to a local variable of the same name.

Live: https://hgt0x.csb.app/, Code: https://codesandbox.io/s/colorpicker-hgt0x

Listing 6.2 The ColorChoiceText component

import React from "react";
 
export default function ColorChoiceText({color}) {     
  return color ? (                                     
    <p>The selected color is {color}!</p>              
  ) : (
    <p>No color has been selected!</p>                 
  )
}

Receive the color state from the parent as a prop.

Check that there is a color.

Use the prop in the UI.

Return alternate UI if the parent doesn’t set a color.

What if the parent doesn’t set a color prop? The ColorChoiceText component is happy for there to be no color prop; it returns alternate UI saying no color was selected.

The ColorSample component, shown in figure 6.4, displays a bar with its background set to the selected color.

Figure 6.4 The ColorSample component displays a bar of the selected color.

ColorSample takes a different approach to a missing prop. It returns no UI at all! In the following listing, you can see the component checking for the color value. If it’s missing, the component returns null and React renders nothing at that point in the element tree.

Live: https://hgt0x.csb.app/, Code: https://codesandbox.io/s/colorpicker-hgt0x

Listing 6.3 The ColorSample component

import React from "react";
 
export default function ColorSample({color}) {    
  return color ? (                                
    <div
      className="colorSample"
      style={{ background: color }}
    />
  ) : null;                                       
}

Receive the state from the parent as a prop.

Check that there is a color.

Don’t render any UI if there’s no color.

You could set a default value for color as part of the prop’s destructuring. Maybe if the parent doesn’t specify a color, then it should be white?

function ColorSample({color = "white"}) {         
  return (
    <div
      className="colorSample"
      style={{ background: color }}
    />
  );
}

Specify a default value for the prop.

A default value will work for some components, but for our color-based components that need to share state, we’d have to make sure all of the defaults were the same. So, we either have alternate UI or no UI. If the component just won’t work without a prop, and a default doesn’t make sense, you can throw an error explaining that the prop is missing.

Although we won’t explore them in this book, you can also use PropTypes to specify expected props and their types. React will use the PropTypes to warn of problems during development (https://reactjs.org/docs/typechecking-with-proptypes.html). Alternatively, use TypeScript rather than JavaScript and type-check your whole application (www.typescriptlang.org).

6.1.3 Receiving an updater function from a parent as a prop

The ColorPicker component uses two state values to generate its UI: a list of available colors and the selected color. It displays the available color values as list items, and the app uses CSS to style them as a row of colored circles, as you can see in figure 6.5. The selected item, goldenrod in the figure, is styled larger than the others.

Figure 6.5 The ColorPicker component displays a list of colors and highlights the selected color.

The Colors component passes the ColorPicker component the two state values it uses. Colors also needs to provide a way to update the selected color for all three children. It delegates that responsibility to the ColorPicker component by passing it the setColor updater function, as illustrated in figure 6.6.

Figure 6.6 The Colors component passes two state values to ColorPicker. It also passes the setColor updater function, so the color state value can be set from the child.

The following listing shows the ColorPicker component destructuring its props argument, assigning the three props to local variables: colors, color, and setColor.

Live: https://hgt0x.csb.app/, Code: https://codesandbox.io/s/colorpicker-hgt0x

Listing 6.4 The ColorPicker component

import React from "react";
 
export default function ColorPicker({colors = [], color, setColor}) {      
  return (
    <ul>
      {colors.map(c => (
        <li
          key={c}
          className={color === c ? "selected" : null}
          style={{ background: c }}
          onClick={() => setColor(c)}                                      
        >
          {c}
        </li>
      ))}
    </ul>
  );
}

Receive the state and updater function from the parent as props.

Use the updater function to set the parent’s state.

The destructuring syntax includes a default value for colors:

{colors = [], color, setColor}

The ColorPicker component iterates over the colors array to create a list item for each available color. Using an empty array as a default value causes the component to return an empty unordered list if the parent component doesn’t set the colors prop.

More interesting (for a book about React Hooks) are the color and setColor props. These props have come from a call to useState in the parent:

const [color, setColor] = useState(availableColors[0]);

The ColorPicker doesn’t care where they’ve come from; it just expects a color prop to hold the current color and a setColor prop to be a function it can call to set the color somewhere. ColorPicker uses the setColor updater function in the onClick handler for each list item. By calling the setColor function, the child component, ColorPicker, is able to set the state for the parent component, Colors. The parent then re-renders, updating all of its children with the newly selected color.

We created the Colors component from scratch, knowing we needed shared state to pass down to child components. Sometimes we work with existing components and, as a project develops, realize they hold state that other siblings may also need. The next sections look at a couple of ways of lifting state up from children to parents to make it more widely available.

6.2 Breaking components into smaller pieces

React gives us the useState and useReducer hooks as two ways of managing state in our apps. Each hook provides a means to update the state, triggering a re-render. As our app develops, we balance the convenience of being able to access local state directly from a single component’s effects, handler functions, and UI against the inconvenience of that component’s state becoming bloated and tangled, with state changes from one part of the UI triggering re-renders of the whole component.

New components in the app may want a piece of the existing state pie, so we now need to share state that, previously, one component encapsulated. Do we lift state values and updater functions up to parents? Or maybe lift reducers and dispatch functions? How does moving state around change the structure of the existing components?

In this section, we continue building out the bookings app example as a context for these questions. In particular, we explore the following:

  • Seeing components as part of a bigger app

  • Organizing multiple components within a page’s UI

  • Creating a BookableDetails component

The concepts encountered are nothing new for existing React developers. Our aim here is to consider if and how they change when using React Hooks.

6.2.1 Seeing components as part of a bigger app

In chapter 5, we left the BookablesList component doing double duty: displaying a list of bookables for the selected group and displaying details for the selected bookable. Figure 6.7 shows the component with the list and details visible.

Figure 6.7 The previous BookablesList component, from chapter 5, showed the list of bookables and the details of the selected bookable.

The component managed all of the state: the bookables, the selected group, and the selected bookable, and flags for displaying details, loading state, and errors. As a single function component with no child components, all of the state was in local scope and available to use when generating the returned UI. But toggling the Show Details check box would cause a re-render of the whole component, and we had to think carefully about persisting timer IDs across renders when using Presentation Mode.

We also need a list of bookables on the Bookings page. Various components will be vying for screen real estate, and we want the flexibility to be able to display the list of bookables separately from the bookable details, as shown in figure 6.8, where the list of bookables is on the left. In fact, as in the figure, we might not want to display the bookable details at all, saving that information for the dedicated Bookables page.

Figure 6.8 The list of bookables (on the left) is also used on the Bookings page.

To be able to use the list and details sections of the BookableList UI independently, we’ll create a separate component for the details of the selected bookable. The BookablesList component will continue to display the groups, list of bookables, and Next button, but the new BookableDetails component will display the details and manage the Show Details check box.

The BookablesPage component currently imports and renders the BookablesList component. We need to do a bit of rearranging to use the new version of the list along with the BookableDetails component.

6.2.2 Organizing multiple components within a page’s UI

Both the BookablesList and the BookableDetails components need access to the selected bookable. We create a BookablesView component to wrap the list and details and to manage the shared state. Table 6.1 lists our proliferating bookables components and outlines how they work together.

Table 6.1 Bookables components and how they work together

Component

Purpose

BookablesPage

Shows the BookablesView component (and, later, forms for adding and editing bookables)

BookablesView

Groups the BookablesList and BookableDetails components and manages their shared state

BookablesList

Shows a list of bookables by group and lets the user select a bookable, either by clicking a bookable or using the Next button

BookableDetails

Shows the details of the selected bookable with a check box to toggle the display of the bookable’s availability

In sections 6.3 and 6.4, we look at two approaches to lifting the state up to the BookablesView component:

  • Lifting the existing reducer from BookablesList to the BookablesView component

  • Lifting the selected bookable from BookablesList to the BookablesView component

First, as shown in the following listing, we update the page component to import and show BookablesView rather than BookablesList.

Branch: 0601-lift-reducer, File: src/components/Bookables/BookablesPage.js

Listing 6.5 The BookablesPage component

import BookablesView from "./BookablesView";     
 
export default function BookablesPage () {
  return (
    <main className="bookables-page">
      <BookablesView/>                           
    </main>
  );
}

Import the new component.

Use the new component.

On separate repo branches, we’ll create a different version of the BookablesView component for each of the two state-sharing approaches we take. The BookableDetails component will be the same either way, so let’s build that first.

6.2.3 Creating a BookableDetails component

The new BookableDetails component performs exactly the same task as the second half of the old BookablesList component UI; it displays the details of the selected bookable and a check box for toggling part of that info. Figure 6.9 shows the BookableDetails component with the check box and bookable title, notes, and availability.

Figure 6.9 The BookableDetails component with check box, title, notes, and availability

As illustrated in figure 6.10, the BookablesView component passes in the selected bookable so that BookableDetails has the information it needs to display.

Figure 6.10 BookablesView manages the shared state and passes the selected bookable to BookableDetails.

The code for the new component is in the following listing. The component receives the selected bookable as a prop but manages its own hasDetails state value.

Branch: 0601-lift-reducer, File: src/components/Bookables/BookableDetails.js

Listing 6.6 The BookableDetails component

import {useState} from "react"; 
import {days, sessions} from "../../static.json";
export default function BookableDetails ({bookable}) {    
  const [hasDetails, setHasDetails] = useState(true);     
 
  function toggleDetails () {
    setHasDetails(has => !has);                           
  }
  return bookable ? (
    <div className="bookable-details item">
      <div className="item-header">
        <h2>{bookable.title}</h2>
        <span className="controls">
          <label>
            <input
              type="checkbox"
              onChange={toggleDetails}                    
              checked={hasDetails}                        
            />
            Show Details
          </label>
        </span>
      </div>
      <p>{bookable.notes}</p>
      {hasDetails && (                                    
        <div className="item-details">
          <h3>Availability</h3>
          <div className="bookable-availability">
            <ul>
              {bookable.days
                .sort()
                .map(d => <li key={d}>{days[d]}</li>)
              }
            </ul>
            <ul>
              {bookable.sessions
                .map(s => <li key={s}>{sessions[s]}</li>)
              }
            </ul>
          </div>
        </div>
      )}
    </div>
  ) : null;
}

Receive the current bookable via props.

Use local state to hold the hasDetails flag.

Use the updater function to toggle the hasDetails flag.

Toggle the hasDetails flag when the check box is clicked.

Use the hasDetails flag to set the check box.

Use the hasDetails flag to show or hide the availability section.

No other components in BookablesView care about the hasDetails state value, so it makes good sense to encapsulate it completely within BookableDetails. If a component is the sole user of a certain state, putting that state within the component seems like an obvious approach.

BookableDetails is a simple component that just displays the selected bookable. As long as it receives that state value, it’s happy. Exactly how the BookablesView component manages that state is more of an open question; should it call useState or useReducer or both? The next two sections explore two approaches. Section 6.4 makes quite a few changes to do away with the reducer. But first, section 6.3 takes an easier path and uses the existing reducer in BookablesList, lifting it up into the BookablesView component.

6.3 Sharing the state and dispatch function from useReducer

We already have a reducer that manages all of the state changes for the BookablesList component. The state the reducer manages includes the bookables data, the selected group, and the index of the selected bookable, along with properties for loading and error states. If we move the reducer up into the BookablesView component, we can use the state the reducer returns to derive the selected bookable and pass it to the child components, as illustrated in figure 6.11.

Figure 6.11 BookablesView manages the state with a reducer and passes the selected bookable or the whole state to its children.

While BookableDetails needs only the selected bookable, BookablesList needs the rest of the state the reducer returns and a way to continue dispatching actions as users select bookables and switch groups. Figure 6.11 also shows BookablesView passing the reducer’s state and dispatch function to BookablesList.

Lifting the state up from BookablesList into the BookablesView component is relatively straightforward. We complete it in three steps:

  • Managing state in the BookablesView component

  • Removing an action from the reducer

  • Receiving state and dispatch in the BookablesList component

Let’s start by updating the BookablesView component to take control of the state.

6.3.1 Managing state in the BookablesView component

The BookablesView component needs to import its two children. It can then pass them the state they need and the means to update that state if required. In the following listing, you can see the imports for the new components, the state that BookablesView is managing, the call to the useReducer hook, and the UI as JSX, with state values and the dispatch function set as props.

Branch: 0601-lift-reducer, File: src/components/Bookables/BookablesView.js

Listing 6.7 Moving the bookables state into the BookablesView component

import {useReducer, Fragment} from "react";
 
import BookablesList from "./BookablesList";                     
import BookableDetails from "./BookableDetails";                 
 
import reducer from "./reducer";                                 
 
const initialState = {                                           
  group: "Rooms",
  bookableIndex: 0,
  bookables: [],
  isLoading: true,
  error: false
};
 
export default function BookablesView () { 
  const [state, dispatch] = useReducer(reducer, initialState);   
 
  const bookablesInGroup = state.bookables.filter(               
    b => b.group === state.group                                 
  );                                                             
  const bookable = bookablesInGroup[state.bookableIndex];        
 
  return (
    <Fragment>
      <BookablesList state={state} dispatch={dispatch}/>         
      <BookableDetails bookable={bookable}/>                     
    </Fragment>
  ); 
}

Import all the components that make up the UI.

Import the reducer that BookablesList was using.

Set up the initial state without hasDetails.

Manage the state and reducer within BookablesView.

Derive the selected bookable from state.

Pass state and dispatch to BookablesList.

Pass the selected bookable to BookableDetails.

The BookablesView component imports the child components it needs and sets up the initial state that used to live in the BookablesList component. We’ve removed the hasDetails property from the state; the new BookableDetails component manages its own state for whether to show details or not.

6.3.2 Removing an action from the reducer

With the BookableDetails component happily toggling its own details, the reducer no longer needs to handle an action for toggling a shared hasDetails state value, so the following case can be removed from reducer.js:

case "TOGGLE_HAS_DETAILS":
  return {
    ...state,
    hasDetails: !state.hasDetails
  };

Apart from that, the reducer can stay as it is. Nice!

6.3.3 Receiving state and dispatch in the BookablesList component

The BookablesList component needs a few tweaks. Instead of relying on its own local reducer and actions, it’s now dependent on the BookablesView component (or any other parent component that renders it). The code for BookablesList is relatively long, so we consider it section by section. The structure of the code looks like this:

export default function BookablesList ({state, dispatch}) {
  // 1. Variables
  // 2. Effect
  // 3. Handler functions
  // 4. UI
}

The following four subsections discuss any changes that are necessary. If you stitch the pieces together, you’ll have the complete component.

Variables

Apart from the two new props, state and dispatch, there are no additions to the variables in the BookablesList component. But with the reducer lifted up to the BookablesView component and the need to display the bookable details removed, there are some deletions. The following listing shows what’s left.

Branch: 0601-lift-reducer, File: src/components/Bookables/BookablesList.js

Listing 6.8 BookablesList: 1. Variables

import {useEffect, useRef} from "react";
import {FaArrowRight} from "react-icons/fa";
import Spinner from "../UI/Spinner";
import getData from "../../utils/api";
 
export default function BookablesList ({state, dispatch}) {          
  const {group, bookableIndex, bookables} = state;
  const {isLoading, error} = state;
 
  const bookablesInGroup = bookables.filter(b => b.group === group);
  const groups = [...new Set(bookables.map(b => b.group))];
 
  const nextButtonRef = useRef();
 
  // 2. Effect
  // 3. Handler functions
  // 4. UI
}

Assign the state and dispatch props to local variables.

The reducer and its initial state are gone, as is the hasDetails flag. Finally, we no longer need to display the bookable details, so we removed the bookable variable.

Effect

The effect is pretty much unchanged apart from one small detail. In the following listing, you can see that we have added the dispatch function to the effect’s dependency array.

Branch: 0601-lift-reducer, File: src/components/Bookables/BookablesList.js

Listing 6.9 BookablesList: 2. Effect

export default function BookablesList ({state, dispatch}) {   
  // 1. Variables
 
  useEffect(() => {
    dispatch({type: "FETCH_BOOKABLES_REQUEST"});
 
    getData("http://localhost:3001/bookables")
      .then(bookables => dispatch({
        type: "FETCH_BOOKABLES_SUCCESS",
        payload: bookables
      }))
      .catch(error => dispatch({
        type: "FETCH_BOOKABLES_ERROR",
        payload: error
      }));
  }, [dispatch]);                                              
 
  // 3. Handler functions
  // 4. UI
}

Assign the dispatch prop to a local variable.

Include dispatch in the dependency array for the effect.

In the previous version, when we called useReducer from within the BookablesList component and assigned the dispatch function to the dispatch variable, React knew that the identity of the dispatch function would never change, so it didn’t need to be declared as a dependency for the effect. Now that a parent component passes dispatch in as a prop, BookablesList doesn’t know where it comes from so can’t be sure it won’t change. Leaving dispatch out of the dependency array prompts a warning in the browser console like the one in figure 6.12.

Figure 6.12 React warns us when dispatch is missing from the dependency array.

Including dispatch in the dependency array is good practice here; we know it won’t change (at least for now), so the effect won’t run unnecessarily. Notice that the warning in figure 6.12 says “If ‘dispatch’ changes too often, find the parent component that defines it and wrap that definition in useCallback.” We look at using the useCallback hook to maintain the identity of functions that are dependencies in section 6.5.

Handler functions

Now that the details for the selected bookable are shown by a different component, we can remove the toggleDetails handler function. Everything else stays the same. Easy!

UI

Goodbye, bookableDetails div! We completely cut out the second section of the UI, for displaying the bookable details. The following listing shows the updated, super-slim BookablesList UI.

Branch: 0601-lift-reducer, File: src/components/Bookables/BookablesList.js

Listing 6.10 BookablesList: 4. UI

export default function BookablesList ({state, dispatch}) {
  // 1. Variables
  // 2. Effect     
  // 3. Handler functions
 
  if (error) {
    return <p>{error.message}</p>
  }
 
  if (isLoading) {
    return <p><Spinner/> Loading bookables...</p>
  }
 
  return (
    <div>
      <select value={group} onChange={changeGroup}>
        {groups.map(g => <option value={g} key={g}>{g}</option>)}
      </select>
      <ul className="bookables items-list-nav">
        {bookablesInGroup.map((b, i) => (
          <li
            key={b.id}
            className={i === bookableIndex ? "selected" : null}
          >
            <button
              className="btn"
              onClick={() => changeBookable(i)}
            >
              {b.title}
            </button>
          </li>
        ))}
      </ul>
      <p>
        <button
          className="btn"
          onClick={nextBookable}
          ref={nextButtonRef}
          autoFocus
        >
          <FaArrowRight/>
          <span>Next</span>
        </button>
      </p>
    </div>
  );
}

All that’s left in the UI is the list of bookables with its associated group picker and Next button. So, we also remove the Fragment component that was grouping the two big chunks of UI.

With the bookable details off on their own adventures and the reducer lifted up to the parent, the changes to the BookablesList component mostly took the form of deletions. One key addition was the inclusion of dispatch in the dependency array for the data-loading effect. Housing the state in the BookablesView component (or maybe even higher up the component tree) seems easy. Stick all the data there, and pass a dispatch function to any descendant components that need to make changes to the state. It’s a valid approach, and one sometimes used by users of popular state-store libraries like Redux. But before throwing all the state up to the top of the app, even if most components don’t care about most of the state that ends up there, let’s investigate an alternative.

6.4 Sharing the state value and updater function from useState

In this section, we try a different approach. We lift only the state that needs to be shared: the selected bookable. Figure 6.13 shows the BookablesView component passing the selected bookable to its two child components. The BookableDetails and BookablesList components still get exactly what they need, and rather than giving BookablesView a whole load of state it doesn’t need to share, BookablesList will manage the rest of the state and functionality that it needs: the loading indicators and errors.

Figure 6.13 BookablesView manages only the shared state. It passes the bookable to the BookableDetails component. It passes the bookable and its updater function to BookablesList.

Lifting the selected bookable up from BookablesList into the BookablesView component requires much less work in BookablesView but a number of changes in BookablesList. We complete the changes in two steps:

  • Managing the selected bookable in the BookablesView component

  • Receiving the bookable and updater function in BookablesList

The BookablesList component still needs a way to let BookablesView know that a user has selected a new bookable. BookablesView passes BookablesList the updater function for the selected bookable. Let’s take a closer look at the latest code for the BookablesView component.

6.4.1 Managing the selected bookable in the BookablesView component

As you can see in listing 6.11, the BookablesView component in this version is very simple; it doesn’t have to deal with the reducer, initial state, or deriving the selected bookable from state. It includes a single call to the useState hook to manage the selected bookable state value. It then passes the selected bookable to both children and the updater function to BookablesList. When a user selects a bookable, the BookablesList component can use the updater function to let BookablesView know that the state has changed.

Branch: 0602-lift-bookable, File: /src/components/Bookables/BookablesView.js

Listing 6.11 Putting the selected bookable in the BookablesView component

import {useState, Fragment} from "react";
 
import BookablesList from "./BookablesList";
import BookableDetails from "./BookableDetails";
 
export default function BookablesView () { 
  const [bookable, setBookable] = useState();                           
 
  return (
    <Fragment>
      <BookablesList bookable={bookable} setBookable={setBookable}/>    
      <BookableDetails bookable={bookable}/>                            
    </Fragment>
  );
}

Manage the selected bookable as a state value.

Pass the bookable and its updater function down.

Pass the bookable down.

BookablesView no longer needs to do the filtering of the bookables for the current group or grab the current bookable from that filtered list. Let’s see how BookablesList changes to adapt to the new approach.

6.4.2 Receiving the bookable and updater function in BookablesList

By letting the BookablesView component manage the selected bookable, we change how the BookablesList component works. In the reducer version, BookablesView stored the bookableIndex and group as part of state. Now, with BookablesList receiving the bookable directly, those state values are no longer needed. The selected bookable looks something like this:

{
  "id": 1,
  "group": "Rooms",
  "title": "Meeting Room",
  "notes": "The one with the big table and interactive screen.",
  "days": [1, 2, 3],
  "sessions": [1, 2, 3, 4, 5, 6]
}

It includes an id and a group property. Whatever group the selected bookable is in is the current group; we don’t need a separate group state value. Also, it’s easy to find the index of the selected bookable within the array of bookables in its group; we don’t need a bookableIndex state value. With the group, bookableIndex, and hasDetails state values no longer needed, resulting in a smaller, simpler state, let’s switch back to using calls to useState rather than a reducer.

There are changes to all sections of the BookablesList component, so we consider the code section by section. The structure of the code looks like this:

export default function BookablesList ({bookable, setBookable}) {
  // 1. Variables
  // 2. Effect
  // 3. Handler functions
  // 4. UI
}

Each of the next four subsections discusses one of the code sections. If you stitch the pieces together, you’ll have the complete component.

Variables

The BookablesList component now receives the selected bookable as a prop. The selected bookable includes an id and a group property. We use the group property to filter the list and the id to highlight the selected bookable.

The following listing shows the updated BookablesList component receiving bookable and setBookable as props and setting up three pieces of local state by calling useState three times.

Branch: 0602-lift-bookable, File: /src/components/Bookables/BookablesList.js

Listing 6.12 BookablesList: 1. Variables

import {useState, useEffect, useRef} from "react";                    
import {FaArrowRight} from "react-icons/fa"; 
import Spinner from "../UI/Spinner";
import getData from "../../utils/api";
 
export default function BookablesList ({bookable, setBookable}) {     
  const [bookables, setBookables] = useState([]);                     
  const [error, setError] = useState(false);                          
  const [isLoading, setIsLoading] = useState(true);                   
 
  const group = bookable?.group;                                      
 
  const bookablesInGroup = bookables.filter(b => b.group === group);
  const groups = [...new Set(bookables.map(b => b.group))];
  const nextButtonRef = useRef();
  // 2. Effect
  // 3. Handler functions
  // 4. UI
}

Import useState rather than useReducer.

Receive the selected bookable and updater function as props.

Manage state with calls to the useState hook.

Get the current group from the selected bookable.

Listing 6.12 grabs the current group from the selected bookable by using the optional chaining operator, ?., a recent addition to JavaScript:

const group = bookable?.group;

If no bookable is selected, the expression bookable?.group returns undefined. It saves us from checking whether the bookable exists before accessing the group property:

const group = bookable && bookable.group;

Until a bookable is selected, the group will be undefined and bookablesInGroup will be an empty array. We need to select a bookable as soon as the bookables data is loaded into the component. Let’s look at the loading process.

Effect

The following listing shows the updated effect code. It now uses updater functions rather than dispatching actions.

Branch: 0602-lift-bookable, File: /src/components/Bookables/BookablesList.js

Listing 6.13 BookablesList: 2. Effect

export default function BookablesList ({bookable, setBookable}) {
  // 1. Variables
 
  useEffect(() => {
    getData("http://localhost:3001/bookables")
 
      .then(bookables => {
        setBookable(bookables[0]);       
        setBookables(bookables);         
        setIsLoading(false);
      })
 
      .catch(error => {
        setError(error);                 
        setIsLoading(false)
      });
 
  }, [setBookable]);                     
 
 
  // 3. Handler functions
  // 4. UI
}

Use the setBookable prop to select the first bookable.

Use the local updater function to set the bookables state.

If there’s an error, set the error state.

Include the external function in the dependency list.

The first effect still uses the getData utility function, created in chapter 4, to load the bookables. But instead of dispatching actions to a reducer, the effect uses all four of the listing’s updater functions: setBookable (passed in as a prop) and setBookables, setIsLoading, and setError (from local calls to useState).

When the data loads, it assigns the data to the bookables state value and calls setBookable with the first bookable in the array:

setBookable(bookables[0]);
setBookables(bookables);
setIsLoading(false);

React is able to sensibly respond to multiple state update calls, like the three just listed. It can batch updates to efficiently schedule any re-renders and DOM changes needed.

As we saw with the dispatch prop in the reducer version in section 6.3, React doesn’t trust functions passed in as props to be the same on each render. In this version, BookingsView passes in the setBookable function as a prop, so we include it in the dependency array for the first effect. Indeed, we sometimes might define our own updater functions rather than directly using those that useState returns. We look at how to make such functions work nicely as dependencies in section 6.5, where we introduce the useCallback hook.

If an error was thrown in the course of loading the data, the catch method sets it as the error state value:

.catch(error => {
  setError(error);
  setIsLoading(false);
);

Handler functions

In the previous version of the BookablesList component, the handler functions dispatched actions to the reducer. In this new version, the handler functions’ key task is to set the bookable. In the following listing, notice how each handler function includes a call to setBookable.

Branch: 0602-lift-bookable, File: /src/components/Bookables/BookablesList.js

Listing 6.14 BookablesList: 3. Handler functions

export default function BookablesList ({bookable, setBookable}) {
  // 1. Variables
  // 2. Effect
 
  function changeGroup (e) {
    const bookablesInSelectedGroup = bookables.filter(
      b => b.group === event.target.value                   
    );
    setBookable(bookablesInSelectedGroup[0]);               
  }
 
  function changeBookable (selectedBookable) {
    setBookable(selectedBookable);
    nextButtonRef.current.focus();
  }
 
  function nextBookable () {
    const i = bookablesInGroup.indexOf(bookable);
    const nextIndex = (i + 1) % bookablesInGroup.length;
    const nextBookable = bookablesInGroup[nextIndex];
    setBookable(nextBookable);
  }
 
  // 4. UI
}

Filter for the selected group.

Set the bookable to the first in the new group.

The current group is derived from the selected bookable; we no longer have a group state value. So when a user chooses a group from the drop-down, the changeGroup function doesn’t directly set the new group. Instead, it selects the first bookable in the chosen group:

setBookable(bookablesInSelectedGroup[0]);

The setBookable updater function is from the BookablesView component and triggers a re-render of BookablesView. BookablesView, in turn, re-renders the BookablesList component, passing it the newly selected bookable as a prop. The BookablesList component uses the bookable’s group and id properties to select the correct group in the drop-down, show just the bookables in the group, and highlight the selected bookable in the list.

The changeBookable function has no surprises: it sets the selected bookable and moves focus to the Next button. In addition to setting the bookable to the next in the current group, nextBookable wraps back to the first if necessary.

UI

We no longer have the bookableIndex value in state. The following listing shows how we use the bookable id instead.

Branch: 0602-lift-bookable, File: /src/components/Bookables/BookablesList.js

Listing 6.15 BookablesList: 4. UI

export default function BookablesList ({bookable, setBookable}) {
  // 1. Variables
  // 2. Effect
  // 3. Handler functions
 
  if (error) {
    return <p>{error.message}</p>
  }
 
  if (isLoading) {
    return <p><Spinner/> Loading bookables...</p>
  }
 
  return (
    <div>
      <select value={group} onChange={changeGroup}>
        {groups.map(g => <option value={g} key={g}>{g}</option>)}
      </select>
 
      <ul className="bookables items-list-nav">
        {bookablesInGroup.map(b => (
          <li
            key={b.id}
            className={b.id === bookable.id ? "selected" : null}   
          >
            <button
              className="btn"
              onClick={() => changeBookable(b)}                    
            >
              {b.title}
            </button>
          </li>
        ))}
      </ul>
      <p>
        <button
          className="btn"
          onClick={nextBookable}
          ref={nextButtonRef}
          autoFocus
        >
          Next
        </button>
      </p>
    </div>
  );
}

Use the ID to check whether a bookable should be highlighted.

Pass the bookable to the changeBookable handler function.

Some key changes to the UI occur in the list of bookables. The code iterates through the bookables in the same group as the selected bookable. One by one, the bookables in the group are assigned to the b variable. The bookable variable represents the selected bookable. If b.id and bookable.id are the same, the current bookable in the list should be highlighted, so we set its class to selected:

className={b.id === bookable.id ? "selected" : null}

When a user clicks a bookable to select it, the onClick handler passes the whole bookable object, b, to the changeBookable function, rather than just the bookable’s index:

onClick={() => changeBookable(b)}

And that’s the BookablesList component without a reducer again. A few changes were made, but with its more focused role of just listing bookables, it’s also simpler overall.

Which approach do you find easier to understand? Dispatching actions to a reducer in the parent or managing most of the state in the component that uses it? In the first approach, we moved the reducer up to the BookablesView component without making many changes. Could we have simplified the state held in the reducer in the same way we did for the variables in the second approach? Whichever implementation you prefer, this chapter gave you a chance to practice calling the useState, useReducer, and useEffect hooks and consider some of the nuances of passing dispatch and updater functions to child components.

Challenge 6.1

Split the UsersList component into UsersList and UserDetails components. Use the UsersPage component to manage the selected user, passing it to UsersList and UserDetails. Find a solution on the 0603-user-details branch.

6.5 Passing functions to useCallback to avoid redefining them

Now that our applications are growing, and we have components working together to provide functionality, it’s natural to be passing state values down to children as props. As we’ve seen in this chapter, those values can include functions. If the functions are updater or dispatch functions from useState or useReducer, React guarantees that their identity will be stable. But for functions we define ourselves, the very nature of components as functions that React calls means our functions will be defined on every render. In this section, we explore the problems such redefining can cause and look at a new hook, useCallback, that can help solve such problems.

6.5.1 Depending on functions we pass in as props

In the previous section, the state for the selected bookable is managed by the BookablesView component. It passes both the bookable and its updater function, setBookable, to BookablesList. BookablesList calls setBookable whenever a user choses a bookable and also within the effect wrapping the data-fetching code, shown here without the catch block:

useEffect(() => {
  getData("http://localhost:3001/bookables")
    .then(bookables => {
      setBookable(bookables[0]);     
      setBookables(bookables);
      setIsLoading(false);
    });
}, [setBookable]);                   

Once the data arrives, set the current bookable to the first.

Include the setBookable function as a dependency.

We include the setBookable updater function as a dependency. The effect reruns whenever the values in its dependency list change. But up to now, setBookable has been an updater function returned by useState and, as such, is guaranteed not to change value; the data-fetching effect runs only once.

The parent component, BookablesView, assigns the updater function to the setBookable variable and sets it directly as one of BookablesList’s props. But it’s not uncommon to do some kind of validation or processing of values before updating state. Say BookablesView wants to check that the bookable exists and, if it does, add a timestamp property before updating state. The following listing shows such a custom setter.

Listing 6.16 Validating and enhancing a value in BookablesView before setting state

import {useState, Fragment} from "react";
 
import BookablesList from "./BookablesList";
import BookableDetails from "./BookableDetails";
 
export default function BookablesView () {
  const [bookable, setBookable] = useState();
 
  function updateBookable (selected) {
    if (selected) {                                                        
      selected.lastShown = Date.now();                                     
      setBookable(selected);                                               
    }
  }
 
  return (
    <Fragment>
      <BookablesList bookable={bookable} setBookable={updateBookable}/>    
      <BookableDetails bookable={bookable}/>
    </Fragment>
  );
}

Check that the bookable exists.

Add a timestamp property.

Set the state.

Pass our handler function as the updater prop.

BookablesView now assigns the custom updateBookable function as the setBookable prop for BookablesList. The BookablesList component cares not a jot, and happily calls the new updater function whenever it wants to select a bookable. So, what’s the problem?

If you update the code to use the new updater function and load the Bookables page, the Network tab of the Developer Tools highlights some disturbing activity: the bookables are being fetched again and again, as shown in figure 6.14.

Figure 6.14 The Network tab of the Developer Tools shows bookables being fetched repeatedly.

The parent component, BookablesView, manages the state for the selected bookable. Whenever BookablesList loads the bookables data and sets the bookable, BookablesView re-renders; React runs its code again, defining the updateBookable function again and passing the new version of the function to BookablesList. The useEffect call in BookablesList sees that the setBookable prop is a new function and runs the effect again, refetching the bookables data and setting the bookable again, restarting the loop. We need a way to maintain the identity of our updater function, so that it doesn’t change from render to render.

6.5.2 Maintaining function identity with the useCallback hook

When we want to use the same function from render to render but don’t want it to be redefined each time, we can pass the function to the useCallback hook. React will return the same function from the hook on every render, redefining it only if one of the function’s dependencies changes. Use the hook like this:

const stableFunction = useCallback(funtionToCache, dependencyList);

The function that useCallback returns is stable while the values in the dependency list don’t change. When the dependencies change, React redefines, caches, and returns the function using the new dependency values. The following listing shows how to use the new hook to solve our endless fetch problem.

Listing 6.17 Maintaining a stable function identity with useCallback

import {useState, useCallback, Fragment} from "react";                    
 
import BookablesList from "./BookablesList";
import BookableDetails from "./BookableDetails";
 
export default function BookablesView () {
  const [bookable, setBookable] = useState();
 
  const updateBookable = useCallback(selected => {                        
    if (selected) {
      selected.lastShown = Date.now();
      setBookable(selected);
    }
  }, []);                                                                 
 
  return (
    <Fragment>
      <BookablesList bookable={bookable} setBookable={updateBookable}/>   
      <BookableDetails bookable={bookable}/>
    </Fragment>
  );
}

Import the useCallback hook.

Pass the updater function to useCallback.

Specify the dependencies.

Assign the stable function as a prop.

Wrapping our updater function in useCallback means React will return the same function on every render, unless the dependencies change values. But we’ve used an empty dependency list, so the values will never change and React will always return the exact same function. The useEffect call in BookablesList will now see that its setBookable dependency is stable, and it’ll stop endlessly refetching the bookables data.

The useCallback hook can be useful, in exactly the same way, when working with components that re-render only when their props change. Such components can be created with React’s memo function, described in the React docs: https://reactjs.org/ docs/react-api.html#reactmemo.

useCallback lets us memoize functions. To prevent the redefinition or recalculation of values more generally, React also provides the useMemo hook, and we’ll look at that in the next chapter.

Summary

  • If components share the same state value, lift the value up to the closest shared ancestor component in the component tree and pass the state down via props:

    const [bookable, setBookable] = useState();
    return (
      <Fragment>
        <BookablesList bookable={bookable}/>
        <BookableDetails bookable={bookable}/>
      </Fragment>
    );
  • Pass the updater function returned by useState to child components if they need to update the shared state:

    const [bookable, setBookable] = useState();
    return <BookablesList bookable={bookable} setBookable={setBookable} />
  • Destructure the props parameter, assigning properties to local variables:

    export default function ColorPicker({colors = [], color, setColor}) {
      return (
        // UI that uses colors, color and setColor
      );
    } 
  • Consider using default values for props. If the prop isn’t set, the default value will be used:

    export default function ColorPicker({colors = [], color, setColor}) {
      return (
        // iterate over colors array
      );
    }
  • Check for undefined or null prop values. Return alternative UI if appropriate:

    export default function ChoiceText({color}) {
      return color ? (
        <p>The selected color is {color}!</p>
      ) : (
        <p>No color has been selected!</p>
      );
    }
  • Return null when it is appropriate to render nothing.

  • To let a child component update the state managed by a parent, pass the child an updater function or a dispatch function. If the function is used in an effect, include the function in the effect’s dependency list.

  • Maintain the identity of functions across renders by wrapping them in calls to the useCallback hook. React will redefine the function only when the dependencies change:

    const stableFunction = useCallback(functionToCache, dependencyList);
..................Content has been hidden....................

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