3 Managing component state with the useReducer hook

This chapter covers

  • Asking React to manage multiple, related state values by calling useReducer
  • Putting component state management logic in a single location
  • Updating state and triggering re-renders by dispatching actions to a reducer
  • Initializing state with initialization arguments and initialization functions

As your applications grow, it’s natural for some components to handle more state, especially if they supply different parts of that state to multiple children. When you find you always need to update multiple state values together or your state update logic is so spread out that it’s hard to follow, it might be time to define a function to manage state updates for you: a reducer function.

A simple, common example is for loading data. Say a component needs to load posts for a blog on things to do when stuck at home during a pandemic. You want to display loading UI when new posts are requested, error UI if a problem arises, and the posts themselves when they arrive. The component’s state includes values for the following:

  • The loading state—Are you in the process of loading new posts?

  • Any errors—Has an error been returned from the server, or is the network down?

  • The posts—A list of the posts retrieved.

When the component requests posts, you might set the loading state to true, the error state to null, and the posts to an empty array. One event causes changes to three pieces of state. When the posts are returned, you might set the loading state to false and the posts to those returned. One event causes changes to two pieces of state. You can definitely manage these state values with calls to the useState hook but, when you always respond to an event with calls to multiple updater functions (setIsLoading, setError, and setPosts, for example), React provides a cleaner alternative: the useReducer hook.

In this chapter, we start by addressing a problem with the BookablesList component in the bookings app: something is amiss with our state management. We then introduce reducers and the useReducer hook as a way of managing our state. Section 3.3 shows how to use a function to initialize the state for a reducer as we start work on a new component, the WeekPicker. We finish the chapter with a review of how the useReducer hook fits in with our understanding of function components.

Can you smell that? There’s a definite whiff in the air. Something’s been left out that should’ve been tidied up. Something stale. Let’s purge that distracting pong!

3.1 Updating multiple state values in response to a single event

You’re free to call useState as many times as you want, once for each piece of state you need React to manage. But a single component may need to hold many values in state, and often those pieces of state are related; you may want to update multiple pieces of state in response to a single user action. You don’t want to leave some pieces of state unattended to when they should really be tidied up.

Our BookablesList component currently has a problem when users switch from one group to another. It’s not a big problem, but in this section we discuss what the problem is, why it’s a problem, and how we can solve it by using the useState hook. That sets us up for the useReducer hook in section 3.2.

3.1.1 Taking users out of the movie with unpredictable state changes

We don’t want clunky, unpredictable interfaces preventing users from getting on with tasks. If the UI keeps pulling their attention away from their desired focus or makes them wait with no feedback or sends them off down dead ends, their thought process is interrupted, their work becomes more difficult, and their day is ruined.

It’s like when you’re watching a movie, and a strange camera movement, or frenzied editing, or blatant product placement, or Ed Sheeran pulls you out of the story. Your train of thought is gone. You become overly aware that it’s a movie, and something’s not quite right. Or when you’re reading a book on programming, and a tortured simile, a strained attempt at humor, perplexing asides, or meta jokes pull you out of the explanation. You become overly aware that you’re reading a desperate author, and something’s not quite right.

Okay, sorry. Back in the room. Let’s see an example. At the end of section 2.3 in the preceding chapter, we diagnosed a mild case of jank in our BookablesList component’s UI. Users are able to choose a group and then select a bookable from that group. The bookable’s details are then displayed. But some combinations of bookable and group selection lead to UI updates that are a little bit off. If you follow these three steps, you should see the UI updates shown in figure 3.1:

  1. Select the Games Room; its details are then displayed.

  2. Switch the group to Kit. The list of Kit bookables is displayed with no bookable selected, and the details disappear.

  3. Click the Next button. The second item of Kit, Wireless Mics, is selected, and its details appear.

Figure 3.1 Selecting a bookable, switching groups, and then clicking the Next button can lead to unpredictable state changes.

Switching from the Rooms group to the Kit group, the component seems to lose track of which bookable is selected. Clicking the Next button then selects the second item, skipping the first. It’s not a huge problem—users can still select bookables—but it may be enough to jar the user out of their focused flow. What’s going on?

It turns out that the selected bookable and the selected group aren’t completely independent values in our state. When a user selects the Games Room, the bookableIndex state value is set to 2; it’s the third item in the list. If they then switch to the Kit group, which has only two items, with indexes 0 and 1, the bookableIndex value no longer matches up with a bookable. The UI ends up with no bookable selected and no details displayed. We need to carefully consider the state we want the UI to be in after a user chooses a group. So, how do we fix our stale index problem and smooth the path for our users?

3.1.2 Keeping users in the movie with predictable state changes

Building a bookings app for our colleagues, we want to make its use as frictionless as possible. Say a colleague, Akiko, has clients visiting next week. She’s organizing her schedule for the visit and needs to book the Meeting Room in the afternoon and then the Games Room after work. Akiko’s focus is on her task: getting the schedule sorted and preparing for a great client visit. The bookings app should let her continue to focus on her task. She should be thinking, “I’ll get those rooms booked and then order the catering,” not “Um, hang on, which button? Did I click it? Has it frozen? Argh, I hate computers!”

It's like when you’re watching a movie and you’re completely invested in a character’s plight. You don’t notice the camera moves and the editing because they help to smoothly draw you into the story. You’re no longer in the movie theater; you’re in the world of the film. The artifice melts away, and the story is everything. Or when you’re reading a book, and its quirky but relatable characters and propulsive plot pull you into the narrative. It’s almost as if the book disappears, and you inhabit the characters’ thoughts, feelings, locations, and actions. Eventually, you notice yourself, and realize you’ve read 100 pages and it’s almost dark. . . .

Okay, sorry. Back in the room. Let’s get back to the example. After the user selects a group, we want the UI to be in a predictable state. We don’t want sudden deselections and skipped bookables. A simple and sensible approach is to always select the first bookable in the list when a user chooses a new group, as shown in figure 3.2.

Figure 3.2 Selecting a bookable, switching groups, and then clicking the Next button leads to predictable state changes.

The group and bookableIndex state values are connected; when we change the group, we change the index as well. In step 2 of figure 3.2, notice that the first item in the list, Projector, is automatically selected when the group is switched. The following listing shows the changeGroup function setting bookableIndex to zero whenever a new group is set.

Branch: 0301-related-state, File: /src/components/Bookables/BookablesList.js

Listing 3.1 Automatically selecting a bookable when the group is changed

import {useState, Fragment} from "react"; 
import {bookables, sessions, days} from "../../static.json";
import {FaArrowRight} from "react-icons/fa";
 
export default function BookablesList () { 
  const [group, setGroup] = useState("Kit");
  const bookablesInGroup = bookables.filter(b => b.group === group);
  const [bookableIndex, setBookableIndex] = useState(0);
  const groups = [...new Set(bookables.map(b => b.group))];
  const bookable = bookablesInGroup[bookableIndex];
  const [hasDetails, setHasDetails] = useState(false);
 
  function changeGroup (event) {       
    setGroup(event.target.value);      
    setBookableIndex(0);               
  }
 
  function nextBookable () {
    setBookableIndex(i => (i + 1) % bookablesInGroup.length);
  }
 
  return (
    <Fragment>
      <div>
        <select
          value={group}
          onChange={changeGroup}       
        >
          {groups.map(g => <option value={g} key={g}>{g}</option>)}
        </select>
 
        <ul className="bookables items-list-nav">
          /* unchanged list UI */
        </ul>
        <p>
          /* unchanged button UI */
        </p>
      </div>
 
      {bookable && (
        <div className="bookable-details">
          /* unchanged bookable details UI */
        </div>
      )}
    </Fragment>
  );
}

Create a handler function to respond to group selection.

Update the group.

Select the first bookable in the new group.

Specify the new function as the onChange handler.

Whenever the group is changed, we set the bookable index to zero; when we call setGroup, we always follow it with a call to setBookableIndex:

setGroup(newGroup);
setBookableIndex(0);

This is a simple example of related state. When components start to get more complicated with multiple events causing multiple state changes, tracking those changes and making sure all related state values are updated together become more and more difficult.

When state values are related in such a way, either affecting each other or often being changed together, it can help to move the state update logic into a single place, rather than spreading the code that performs changes across event handler functions, whether inline or separately defined. React gives us the useReducer hook to help us manage this collocation of state update logic, and we look at that hook next.

3.2 Managing more complicated state with useReducer

As it stands, the BookablesList component example is simple enough that you could continue to use useState and just call the respective updater functions for each piece of state within the changeGroup event handler. But when you have multiple pieces of interrelated state, using a reducer can make it easier to make and understand state changes. In this section, we introduce the following topics:

  • A reducer helps you to manage state changes in a centralized, well-defined way with clear actions that act on the state.

  • A reducer uses actions to generate a new state from the previous state, making it easier to specify more complicated updates that may involve multiple pieces of interrelated state.

  • React provides the useReducer hook to let your component specify initial state, access the current state, and dispatch actions to update the state and trigger a re-render.

  • Dispatching well-defined actions makes it easier to follow state changes and to understand how your component interacts with the state in response to different events.

We start, in section 3.2.1, with a description of a reducer and a simple example of a reducer that manages incrementing and decrementing a counter. In section 3.2.2, we build a reducer for the BookablesList component that performs the necessary state changes like switching groups, selecting bookables, and toggling bookable details. Finally, in section 3.2.3, we incorporate our freshly minted reducer into the BookablesList component by using React’s useReducer hook.

3.2.1 Updating state using a reducer with a predefined set of actions

A reducer is a function that accepts a state value and an action value. It generates a new state value based on the two values passed in. It then returns the new state value, as shown in figure 3.3.

Figure 3.3 A reducer takes a state and an action and returns a new state.

The state and action can be simple, primitive values like numbers or strings, or more complicated objects. With a reducer, you keep all of the ways of updating the state in one place, which makes it easier to manage state changes, particularly when a single action affects multiple pieces of state.

We get back to the BookablesList component shortly, after a super-simple example. Say your state’s just a counter and there are only two actions you can take: increment the counter or decrement the counter. The following listing shows a reducer that manages such a counter. The value of the count variable starts at 0 and changes to 1, to 2, and then back to 1.

Code on JS Bin: https://jsbin.com/capogug/edit?js,console

Listing 3.2 A simple reducer for a counter

let count = 0;
 
function reducer (state, action) {         
  if (action === "inc") {                  
    return state + 1;
  }
  if (action === "dec") {                  
    return state - 1;
  }
  return state;                            
}
 
count = reducer(count, "inc");             
count = reducer(count, "inc");
count = reducer(count, "dec");             

Create a reducer function that accepts the existing state and an action.

Check which action is specified and update state accordingly.

Handle missing or unrecognized actions.

Use the reducer to increment the counter.

Use the reducer to decrement the counter.

The reducer handles the incrementing and decrementing actions and just returns the count unaltered for any other action specified. (Rather than silently ignoring unrecognized actions, you could throw an error, depending on the needs of your application and the role the reducer is playing.)

That seems like a bit of overkill for our two little actions, but having a reducer makes it easy to extend. Let’s add three more actions, for adding and subtracting arbitrary numbers to and from the counter and for setting the counter to a specified value. To be able to specify extra values with our action, we need to beef it up a bit—let’s make it an object with a type and a payload. Say we want to add 3 to the counter; our action looks like this:

{
  type: "add",
  payload: 3
}

The following listing shows the new reducer with its extra powers and calls to the reducer passing our beefed-up actions. The value of the count variable starts at 0 and changes to 3, to -7, to 41, and finally to 42.

Code on JS Bin: https://jsbin.com/kokumux/edit?js,console

Listing 3.3 Adding more actions and specifying extra values

let count = 0;
 
function reducer (state, action) {
  if (action.type === "inc") {                              
    return state + 1;                                       
  }                                                         
 
  if (action.type === "dec") {                              
    return state - 1;                                       
  }                                                         
 
  if (action.type === "add") {                              
    return state + action.payload;                          
  }                                                         
 
  if (action.type === "sub") {                              
    return state - action.payload;                          
  }                                                         
 
  if (action.type === "set") {                              
    return action.payload;                                  
  }                                                         
 
  return state;
}
 
count = reducer(count, { type: "add", payload: 3 });        
count = reducer(count, { type: "sub", payload: 10 });       
count = reducer(count, { type: "set", payload: 41 });       
count = reducer(count, { type: "inc" });                    

Now check the action type for the two original actions.

Use the action payload to perform the new actions.

Pass an object to specify each action.

The last call to the reducer right at the end of listing 3.3 specifies the increment action. The increment action doesn’t need any extra information. It always adds 1 to count, so the action doesn’t include a payload property.

Let’s put these ideas of state and actions with a type and payload into practice in the bookings app by building a reducer for our BookablesList component. Then we can see how to enlist React’s help to use that reducer to manage the component’s state.

3.2.2 Building a reducer for the BookablesList component

The BookablesList component has four pieces of state: group, bookableIndex, hasDetails, and bookables (imported from static.json). The component also has four actions to perform on that state: set the group, set the index, toggle hasDetails, and move to the next bookable. To manage four pieces of state, we can use an object with four properties. It’s common to represent both the state and the action as objects, as shown in figure 3.4.

Figure 3.4 Pass the reducer a state object and an action object. The reducer updates the state based on the action type and payload. The reducer returns the new, updated state.

The BookablesList component imports the bookables data from the static.json file. That data won’t change while the BookablesList component is mounted, and we include it in the initial state for the reducer, using it to find the number of bookables in each group.

The following listing shows a reducer for the BookablesList component using objects for both the state and the actions. We export it from its own file, reducer.js, in the /src/components/Bookables folder.

Branch: 0302-reducer, File: /src/components/Bookables/reducer.js

Listing 3.4 A reducer for the BookablesList component

export default function reducer (state, action) {
  switch (action.type) {                                    ❶❷
 
    case "SET_GROUP":                                       
      return {
        ...state,
        group: action.payload,                              
        bookableIndex: 0
      };
 
    case "SET_BOOKABLE":
      return {
        ...state,                                           
        bookableIndex: action.payload
      };
 
    case "TOGGLE_HAS_DETAILS":
      return {
        ...state,
        hasDetails: !state.hasDetails                      
      };
 
    case "NEXT_BOOKABLE":
      const count = state.bookables.filter(
        b => b.group === state.group
      ).length;                                            
 
      return {
        ...state,
        bookableIndex: (state.bookableIndex + 1) % count   
      };
 
    default:                                               
      return state;
  }
}

Use a switch statement to organize the code for each action type.

Specify the action type as the comparison for each case.

Create a case block for each action type.

Update the group and set the bookableIndex to 0.

Use the spread operator to copy existing state properties.

Override existing state properties with any changes.

Count the bookables in the current group.

Use the count to wrap from the last index to the first.

Always include a default case.

Each case block returns a new JavaScript object; the previous state is not mutated. The object spread operator is used to copy across properties from the old state to the new. You then set the property values that need updating on the object, overriding those from the previous state, like this:

return {
  ...state,                  
  group: action.payload,     
  bookableIndex: 0           
};

Spread the properties of the old state object into the new one.

Override any properties that need updating.

With only four properties in total in our state, we could have set them all explicitly:

return {
  group: action.payload,
  bookableIndex: 0,
  hasDetails: state.hasDetails,    
  bookables: state.bookables       
};

Copy across previous values for unchanged properties.

Using the spread operator protects the code as it evolves; the state may gain new properties in the future, and they all need to be copied across.

Notice that the SET_GROUP action updates two properties. In addition to updating the group to be displayed, it sets the selected bookable index to 0. When switching to a new group, the action automatically selects the first bookable and, as long as the group has at least one bookable, the component shows the details for the first bookable if the Show Details toggle is checked.

The reducer also handles a NEXT_BOOKABLE action, removing from the Bookables component the onus for calculating indexes when moving from one bookable to the next. This is why including the bookables data in the reducer’s state is helpful; we use the count of bookables in a group to wrap from the last bookable to the first when incrementing bookableIndex:

case "NEXT_BOOKABLE":
  const count = state.bookables.filter(                   
    b => b.group === state.group
  ).length;
  
  return {
    ...state,
    bookableIndex: (state.bookableIndex + 1) % count      
  };

Use the bookables data to count the bookables in the current group.

Use the modulus operator to wrap from the last index to the first.

We have a reducer set up, but how do we fold it into our component? How do we access the state object and call the reducer with our actions? We need the useReducer hook.

3.2.3 Accessing component state and dispatching actions with useReducer

The useState hook lets us ask React to manage single values for our component. With the useReducer hook, we can give React a bit more help in managing values by passing it a reducer and the component’s initial state. When events occur in our application, instead of giving React new values to set, we dispatch an action, and React uses the corresponding code in the reducer to generate a new state before calling the component for the latest UI.

When calling the useReducer hook, we pass it the reducer and an initial state. The hook returns the current state and a function for dispatching actions, as two elements in an array, as shown in figure 3.5.

Figure 3.5 Call useReducer with a reducer and an initial state. It returns the current state and a dispatch function. Use the dispatch function to dispatch actions to the reducer.

As we did with useState, here with useReducer we use array destructuring to assign the two elements of the returned array to two variables with names of our choosing. The first element, the current state, we assign to a variable we call state, and the second element, the dispatch function, we assign to a variable we call dispatch:

const [state, dispatch] = useReducer(reducer, initialState);

React pays attention to only the arguments passed to useReducer (in our case, reducer and initialState) the first time React invokes the component. On subsequent invocations, it ignores the arguments but still returns the current state and the dispatch function for the reducer.

Let’s get the useReducer hook up and running in the BookablesList component and start dispatching some actions! The following listing shows the changes.

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

Listing 3.5 The BookablesList component using a reducer

import {useReducer, Fragment} from "react";                        
import {bookables, sessions, days} from "../../static.json";
import {FaArrowRight} from "react-icons/fa";
 
import reducer from "./reducer";                                   
 
const initialState = {                                             
  group: "Rooms",                                                  
  bookableIndex: 0,                                                
  hasDetails: true,                                                
  bookables                                                        
};                                                                 
 
export default function BookablesList () {
  const [state, dispatch] = useReducer(reducer, initialState);     
 
  const {group, bookableIndex, bookables, hasDetails} = state;     
 
  const bookablesInGroup = bookables.filter(b => b.group === group);
  const bookable = bookablesInGroup[bookableIndex];
  const groups = [...new Set(bookables.map(b => b.group))];
 
  function changeGroup (e) {
    dispatch({                                                     
      type: "SET_GROUP",                                           
      payload: e.target.value                                      
    });                                                            
  }
 
  function changeBookable (selectedIndex) {
    dispatch({
      type: "SET_BOOKABLE",
      payload: selectedIndex
    });
  } 
 
  function nextBookable () {
    dispatch({ type: "NEXT_BOOKABLE" });                           
  }
 
  function toggleDetails () {
    dispatch({ type: "TOGGLE_HAS_DETAILS" });
  }
 
  return (
    <Fragment>
      <div>
        // group picker
 
        <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>
 
        // Next button
      </div>
 
      {bookable && (
        <div className="bookable-details">
          <div className="item">
            <div className="item-header">
              <h2>
                {bookable.title}
              </h2>
              <span className="controls">
                <label>
                  <input
                    type="checkbox"
                    checked={hasDetails}
                    onChange={toggleDetails}                     
                  />
                  Show Details
                </label>
              </span>
            </div>
            <p>{bookable.notes}</p>
            {hasDetails && (
              <div className="item-details">
                // details
              </div>
            )}
          </div>
        </div>
      )}
    </Fragment>
  );
}

Import the useReducer hook.

Import the reducer from listing 3.4.

Specify an initial state.

Call useReducer, passing the reducer and the initial state.

Assign state values to local variables.

Dispatch an action with a type and a payload.

Dispatch an action that doesn’t need a payload.

Call the new changeBookable function.

Call the new toggleDetails function.

Listing 3.5 imports the reducer we created in listing 3.4, sets up an initial state object, and then, in the component code itself, passes the reducer and initial state to useReducer. Next, useReducer returns the current state and the dispatch function, and we assign them to variables, state and dispatch, using array destructuring. The listing uses an intermediate state variable and then destructures the state object into individual variables—group, bookableIndex, bookables, and hasDetails—but you could do the object destructuring directly inside the array destructuring:

const [
  {group, bookableIndex, bookables, hasDetails},
  dispatch
] = useReducer(reducer, initialState);

In the event handlers, the BookablesList component now dispatches actions rather than updating individual state values via useState. We use separate event handler functions (changeGroup, changeBookable, nextBookable, toggleDetails), but you could easily dispatch actions inline within the UI. For example, you could set up the Show Details check box like this:

 <label>
   <input
     type="checkbox"
     checked={hasDetails}
     onChange={() => dispatch({ type: "TOGGLE_HAS_DETAILS" })}
   />
   Show Details
 </label>

Either approach is fine, as long as you (and your team) find the code easy to read and understand.

Although the example is simple, you should appreciate how a reducer can help structure your code, your state mutations, and your understanding, particularly as the component state becomes more complex. If your state is complex and/or the initial state is expensive to set up or is generated by a function you’d like to reuse or import, the useReducer hook has a third argument you can use. Let’s check it out.

3.3 Generating the initial state with a function

You saw in chapter 2 that we can generate the initial state for useState by passing a function to the hook. Similarly, with useReducer, as well as passing an initialization argument as the second argument, we can pass an initialization function as the third argument. The initialization function uses the initialization argument to generate the initial state, as shown in figure 3.6.

Figure 3.6 The initialization function for useReducer uses the initialization argument to generate the reducer’s initial state.

As usual, useReducer returns an array with two elements: the state and a dispatch function. On the first call, the state is the return value of the initialization function. On subsequent calls, it is the state at the time of the call:

const [state, dispatch] = useReducer(reducer, initArgument, initFunction);

Use the dispatch function to dispatch actions to the reducer. For a particular call to useReducer, React will always return the same dispatch function. (Having an unchanging function is important when re-renders may depend on changing props or dependencies, as you’ll see in later chapters.)

In this section, we put useReducer’s initialization function argument to use as we start work on a second component for the bookings app, the WeekPicker component. We split the work into five subsections:

  • Introducing the WeekPicker component

  • Creating utility functions to work with dates and weeks

  • Building the reducer to manage dates for the component

  • Creating WeekPicker, passing an initialization function to the useReducer hook

  • Updating BookingsPage to use WeekPicker

3.3.1 Introducing the WeekPicker component

So far in the bookings app, we’ve been concentrating on the BookablesList component, displaying a list of bookables. To set the groundwork for actually booking a resource, we need to start thinking about calendars; in the finished app, our users will pick a date and session from a bookings grid calendar, as shown in figure 3.7.

Figure 3.7 The bookings page will include a list of bookables, a bookings grid, and a week picker.

Let’s start small and just consider the interface for switching between one week and the next. Figure 3.8 shows a possible interface for picking the week to show in the bookings grid. It includes the following:

  • The start and end dates for the selected week

  • Buttons to move to the next and previous weeks

  • A button to show the week containing today’s date

Figure 3.8 The WeekPicker component shows the start and end dates for the chosen week and has buttons to navigate between weeks.

Later in the book, we’ll add an input for jumping straight to a specific date. For now, we’ll stick with our three buttons and week date text. To get the start and end dates for a specified week, we need a couple of utility functions to wrangle JavaScript’s date object. Let’s conjure those first.

3.3.2 Creating utility functions to work with dates and weeks

Our bookings grid will show one week at a time, running from Sunday to Saturday. On any particular date, we show the week that contains that date. Let’s create objects that represent a week, with a particular date in the week and the dates for the start and end of the week:

week = {
  date,       
  start,      
  end         
};

JavaScript Date object for a particular date

Date object for the start of the week containing date

Date object for the end of the week

For example, take Wednesday, April 1, 2020. The start of the week was Sunday, March 29, 2020, and the end of the week was Saturday, April 4, 2020:

week = {
  date,   // 2020-04-01     
  start,  // 2020-03-29     
  end     // 2020-04-04     
};

Assign each property a JavaScript Date object for the specified date.

The following listing shows a couple of utility functions: one to create a new date from an old date, offset by a number of days, and the second to generate the week objects. The file is called date-wrangler.js and is in a new /src/utils folder.

Branch: 0303-week-picker, File: /src/utils/date-wrangler.js

Listing 3.6 Date-wrangling utility functions

export function addDays (date, daysToAdd) {
  const clone = new Date(date.getTime());
  clone.setDate(clone.getDate() + daysToAdd);        
  return clone;
}
 
export function getWeek (forDate, daysOffset = 0) {
  const date = addDays(forDate, daysOffset);         
  const day = date.getDay();                         
 
  return {
    date,
    start: addDays(date, -day),                      
    end: addDays(date, 6 - day)                      
  };
}

Shift the date by the number of days specified.

Immediately shift the date.

Get the day index for the new date, for example, Tuesday = 2.

For example, if it’s Tuesday, shift back by 2 days.

For example, if it’s Tuesday, shift forward by 4 days.

The getWeek function uses the getDay method of JavaScript’s Date object to get the day-of-the-week index of the specified date: Sunday is 0, Monday is 1, . . . , Saturday is 6. To get to the start of the week, the function subtracts the same number of days as the day index: for Sunday, it subtracts 0 days; for Monday, it subtracts 1 day; . . . ; for Saturday, it subtracts 6 days. The end of the week is 6 days after the start of the week, so to get the end of the week, the function performs the same subtraction as for the start of the week but also adds 6. We can use the getWeek function to generate a week object for a given date:

const today = new Date();
const week = getWeek(today);     

Get the week object for the week containing today’s date.

We can also specify an offset number of days as the second argument if we want the week object for a date relative to the date in the first argument:

const today = new Date();
const week = getWeek(today, 7);     

Get the week object for the week containing the date a week from today.

The getWeek function lets us generate week objects as we navigate from week to week in the bookings app. Let’s use it to do just that in a reducer.

3.3.3 Building the reducer to manage dates for the component

A reducer helps us to centralize the state management logic for our WeekPicker component. In a single place, we can see all of the possible actions and how they update the state:

  • Move to the next week by adding seven days to the current date.

  • Move to the previous week by subtracting seven days from the current date.

  • Move to today by setting the current date to today’s date.

  • Move to a specified date by setting the current date to the action’s payload.

For each action, the reducer returns a week object as described in the previous section. Although we really need to track only a single date, we would need to generate the week object at some point, and abstracting the week object generation along with the reducer seems sensible to me. You can see how the possible state changes translate to a reducer in the following listing. We put the weekReducer.js file in the Bookings folder.

Branch: 0303-week-picker, File: /src/components/Bookings/weekReducer.js

Listing 3.7 The reducer for WeekPicker

import {getWeek} from "../../utils/date-wrangler";          
 
export default function reducer (state, action) {
  switch (action.type) {
    case "NEXT_WEEK":
      return getWeek(state.date, 7);                        
    case "PREV_WEEK":
      return getWeek(state.date, -7);                       
    case "TODAY":
      return getWeek(new Date());                           
    case "SET_DATE":
      return getWeek(new Date(action.payload));             
    default:
      throw new Error(`Unknown action type: ${action.type}`)
  }
}

Import the getWeek function.

Return a week object for 7 days ahead.

Return a week object for 7 days before.

Return a week object for today.

Return a week object for a specified date.

The reducer imports the getWeek function to generate the week object for each state change. Having the getWeek function available to import means we can also use it as an initialization function when we call the useReducer hook in the WeekPicker component.

3.3.4 Passing an initialization function to the useReducer hook

The WeekPicker component lets users navigate from week to week to book resources in the company. We set up the reducer in the preceding section; now it’s time to use it. The reducer needs an initial state, a week object. The following listing shows how we can use the getWeek function to generate the initial week object from a date we pass to WeekPicker as a prop. The WeekPicker.js file is also in the Bookings folder.

Branch: 0303-week-picker, File: /src/components/Bookings/WeekPicker.js

Listing 3.8 The WeekPicker component

import {useReducer} from "react";
import reducer from "./weekReducer";
import {getWeek} from "../../utils/date-wrangler";                   
import {FaChevronLeft, FaCalendarDay, FaChevronRight} from "react-icons/fa";
 
export default function WeekPicker ({date}) {                        
  const [week, dispatch] = useReducer(reducer, date, getWeek);       
 
  return (
    <div>
      <p className="date-picker">
        <button
          className="btn"
          onClick={() => dispatch({type: "PREV_WEEK"})}              
        >
          <FaChevronLeft/>
          <span>Prev</span>
        </button>
 
        <button
          className="btn" 
          onClick={() => dispatch({type: "TODAY"})}                  
        >
          <FaCalendarDay/>
          <span>Today</span>
        </button>
        <button
          className="btn"
          onClick={() => dispatch({type: "NEXT_WEEK"})}              
        >
          <span>Next</span>
          <FaChevronRight/>
        </button>
      </p>
      <p>
        {week.start.toDateString()} - {week.end.toDateString()}      
      </p>
    </div>
  );
}

Import the getWeek date-wrangler function.

Receive the initial date as a prop.

Generate the initial state, passing date to getWeek.

Dispatch actions to the reducer to switch weeks.

Use the current state to display the date info.

Our call to useReducer passes the specified date to the getWeek function. The getWeek function returns a week object that is set as the initial state. We assign the state that useReducer returns to a variable called week:

const [week, dispatch] = useReducer(reducer, date, getWeek);

In addition to letting us reuse the getWeek function to generate state (in the reducer and the WeekPicker component), the initialization function (useReducer’s third argument) also allows us to run expensive state generation functions once only, on the initial call to useReducer.

At last, a new component! Let’s hook it up to BookingsPage.

3.3.5 Updating BookingsPage to use WeekPicker

The following listing shows an updated BookingsPage component that imports and renders the WeekPicker component. The resulting page is shown in figure 3.9.

Branch: 0303-week-picker, File: /src/components/Bookings/BookingsPage.js

Listing 3.9 The BookingsPage component using WeekPicker

import WeekPicker from "./WeekPicker";        
 
export default function BookingsPage () {
  return (
    <main className="bookings-page">
      <p>Bookings!</p>
      <WeekPicker date={new Date()}/>         
    </main>
  );
}

Import the WeekPicker component.

Include the WeekPicker in the UI, passing it the current date.

 

Figure 3.9 The BookingsPage component with the WeekPicker component in place

BookingsPage passes the WeekPicker component the current date. The week picker first appears showing the start and end dates of the current week, from Sunday to Saturday. Have a go navigating from week to week and then click the Today button to jump back to the present week. It’s a simple component but helps drive the bookings grid in chapters to come. And it provides an example of useReducer’s initialization function argument.

Before this chapter’s formal Summary section, let’s briefly recap some of the key concepts we’ve encountered, building your understanding of function components and hooks.

3.4 Reviewing some useReducer concepts

A bit more jargon has crept into this discussion, so just in case all the actions, reducers, and dispatch functions are causing some dizziness, table 3.1 describes the terms with examples. Take a breather!

Table 3.1 Some of the key terms we’ve met

Icon

Term

Description

Example

Initial state

The values of variables and properties when the component first runs

{

group: "Rooms",

bookableIndex: 0,

hasDetails: false

}

Action

Information that the reducer uses to update the state

{

type: "SET_BOOKABLE",

payload: 1

}

Reducer

A function React passes the current state and an action. It creates a new state from the current state, depending on the action.

(state, action) => {

// check action

// update state based

// on action type and

// action payload

// return new state

};

State

The values of variables and

properties at a particular point

in execution

{

group: "Rooms",

bookableIndex: 1,

hasDetails: false

}

Dispatch

function

A function for dispatching actions to the reducer. Use it to tell the reducer what action to take.

dispatch({

type: "SET_BOOKABLE",

payload: 1

});

Once we pass the reducer and initial state to React via our call to useReducer, it manages the state for us. We just have to dispatch actions, and React will use the reducer to update the state depending on which action it receives. Remember, our component code returns a description of its UI. Having updated the state, React knows it may need to update the UI, so it will call our component code again, passing it the latest state and the dispatcher function when the component calls useReducer. To reinforce the functional nature of our components, figure 3.10 illustrates each step when React first calls the BookablesList component and a user then fires an event by selecting a group, choosing a bookable, or toggling the Show Details check box.

Table 3.2 lists the steps from figure 3.10, describing what is happening and including short discussions of each one.

Figure 3.10 Stepping through the key moments when using useReducer

Each time it needs the UI, React invokes the component code. The component function runs to completion, and local variables are created during execution, and destroyed or referenced in closures when the function ends. The function returns a description of the UI for the component. The component uses hooks, like useState and useReducer, to persist state across invocations and to receive updater and dispatch functions. Event handlers call the updater functions or dispatch actions in response to user actions, and React can update the state and call the component code again, restarting the cycle.

Table 3.2 Some key steps when using useReducer

Step

What happens?

Discussion

1

React calls the component.

To generate the UI for the page, React traverses the tree of components, calling each one. React will pass each component any props set as attributes in the JSX.

2

The component calls useReducer for the first time.

The component passes the initial state and the reducer to the useReducer function. React sets the current state for the reducer as the initial state.

3

React returns the current state and the dispatch function as an array.

The component code assigns the state and dispatch function to variables for later use. The variables are often called state and dispatch, or we might destructure the state into further variables.

4

The component sets up an event handler.

The event handler may listen for user clicks, timers firing, or resources loading, for example. The handler will dispatch an action to change the state.

5

The component returns its UI.

The component uses the current state to generate its user interface and returns it, finishing its work. React compares the new UI to the old and updates the DOM.

6

The event handler dispatches an action.

An event fires, and the handler runs. The handler uses the dispatch function to dispatch an action.

7

React calls the reducer.

React passes the current state and the dispatched action to the reducer.

8

The reducer returns the new state.

The reducer uses the action to update the state and returns the new version.

9

React calls the component.

React knows the state has changed and so must recalculate the UI.

10

The component calls useReducer for the second time.

This time, React will ignore the arguments.

11

React returns the current state and the dispatch function.

The state has been updated by the reducer, and the component needs the latest values. The dispatch function is the exact same function as React returned for the previous call to useReducer.

12

The component sets up an event handler.

This is a new version of the handler and may use some of the newly updated state values.

13

The component returns its UI.

The component uses the current state to generate its user interface and returns it, finishing its work. React compares the new UI to the old and updates the DOM.

Summary

  • If you have multiple pieces of interrelated state, consider using a reducer to clearly define the actions that can change the state. A reducer is a function to which you pass the current state and an action. It uses the action to generate a new state. It returns the new state:

    function reducer (state, action) {
      // use the action to generate a new state from the old state.
      // return newState.
    }
  • Call the useReducer hook when you want React to manage the state and reducer for a component. Pass it the reducer and an initial state. It returns an array with two elements, the state and a dispatch function:

    const [state, dispatch] = useReducer(reducer, initialState);
  • Call the useReducer hook with an initialization argument and an initialization function to generate the initial state when the hook is first called. The hook automatically passes the initialization argument to the initialization function. The initialization function returns the initial state for the reducer. This is useful when initialization is expensive or when you want to use an existing function to initialize the state:

    const [state, dispatch] = useReducer(reducer, initArg, initFunc);
  • Use the dispatch function to dispatch an action. React will pass the current state and the action to the reducer. It will replace the state with the new state generated by the reducer. It will re-render if the state has changed:

    dispatch(action);
  • For anything more than the most basic actions, consider following common practice and specify the action as a JavaScript object with type and payload properties:

    dispatch({ type: "SET_NAME", payload: "Jamal" });
  • React always returns the same dispatch function for a particular call to useReducer within a component. (If the dispatch function changed between calls, it could cause unnecessary re-renders when passed as a prop or included as a dependency for other hooks.)

  • In the reducer, use if or switch statements to check for the type of action dispatched:

    function reducer (state, action) {
      switch (action.type) {
        case "SET_NAME":
          return {
            ...state,
            name: action.payload
          }
        default:
          return state;
          // or return new Error(`Unknown action type: ${action.type}`)
      }
    }

    In the default case, either return the unchanged state (if the reducer will be combined with other reducers, for example) or throw an error (if the reducer should never receive an unknown action type).

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

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