useCallback
hookUp 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. . . .
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:
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/).
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
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.
❸ 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.
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.
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.
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
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.
❹ 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.
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
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).
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.
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.
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
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.
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:
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.
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.
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.
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.
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.
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
import BookablesView from "./BookablesView"; ❶ export default function BookablesPage () { return ( <main className="bookables-page"> <BookablesView/> ❷ </main> ); }
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.
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.
As illustrated in figure 6.10, the BookablesView
component passes in the selected bookable so that BookableDetails
has the information it needs to display.
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
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.
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.
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:
Let’s start by updating the BookablesView
component to take control of the state.
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
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.
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!
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.
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
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.
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
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.
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.
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!
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
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.
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.
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.
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
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.
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.
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.
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
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.
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
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); );
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
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.
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
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.
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.
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.
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.
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.
❹ 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.
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.
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.
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.
❹ 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.
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> ); }
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);
3.142.173.227