Introduction to Managing State in React

Get an introduction to managing state in React.

Managing State in React

As we’ve built up the React page in our app, we’ve been passing properties and handlers up and down the DOM tree to allow our data to be shared between components. This is somewhat complicated and error-prone. We can use a global data store to keep us from having to do all this property passing, all we’ll need to do is make sure each component subscribes to the global store and sends actions to it to change state. React comes with built-in features to support this kind of global data sharing, and it contains hook methods that we can use in our functional components to access this data.

To see how global data can help, we’re going to add a new feature to our concert page that builds up a price subtotal based on the number of tickets the user has on hold and allows the user to clear all tickets from the subtotal section. As currently implemented, this would involve even more passing of data up and down the DOM tree, but we can use contexts and reducers to centralize the data.

Using Reducers

Global data has different problems than local data. In particular, we need to make sure that changes to the global data store happen consistently for all users so that different parts of the application have access to data in the same state.

We’re going to solve our global data problem by refactoring our data using a JavaScript pattern called a reducer and a related structure called a store.

A reducer is a JavaScript function that takes two arguments. The first argument is an object representing a state. The second argument represents an action to take. The return value is a new state object representing the state of the data after the action is performed. The action is meant to describe the state change in terms of application logic, while the reducer converts that to changes in data.

Let’s look at a simple example in which we will count American coins. The state of the world is the total number of coins and the total value of the coins. A reducer to partially handle this task might look like this:

const initialState = {count: 0, value: 0}

const reducer = (state, action) {
  switch (action.type) {
	  case "AddPenny": {
	    return { count: state.count + 1, value: state.value + 1 }
    }
    case "AddNickel": {
	    return { count: state.count + 1, value: state.value + 5 }
    }
    // and so on...
  }
}

Then we’d call this method something like:

const withAPenny = reducer(initialState, {type: "AddPenny"})
const pennyAndNickel = reducer(withAPenny, {type: "AddNickel"})

The idea is that we only change state with the reducer function, which is a constraint we have to enforce ourselves. Each call to the reducer function returns a new instance of the state object that is separate from all the other instances.

This is somewhat verbose, so why use this structure at all? While verbose, the basic idea of making the central state immutable and only accessible with specific methods is a good one. We’ll stick with this pattern, as it’s common in other JavaScript codes.

One problem with a reducer on its own is the possibility that different callers to the reducer might get out of sync. A solution is to also maintain a store. For our purposes, a store is a centralized object that manages access to both a single source of central data and a related reducer. For our coin example, a store might look like this:

export class CoinStore {
  static state = {count: 0, value: 0}
  static getState(): { return state }
  static dispatch(action) {
    CoinStore.state = reducer(CoinStore.state, action)
    return CoinStore.state
  }
}

Again, we’re holding off on TypeScript annotations. This code sets up a store with a single point of access:

CoinStore.dispatch({type: "AddPenny"})
CoinStore.dispatch({type: "AddNickel"})
const finalState = CoinStore.getState()

In this case, we know that our return value from dispatch is the current state, but typically we only ask for the state. Otherwise, we act on it only through actions.

Using Context to Share State in React

For our app, we’re going to use two React hooks to help share the state. The first hook we’re going to use is useContext, which lets us use a feature of React called a context. In React, contexts allow us to share global data among components without passing that data via props.

To use a context, we surround our code with a special JSX component called a context provider. The context provider is initialized with a value and any component inside that provider, no matter how many levels down, can use the useContext hook to give that component inside access to the data in the context.

The specific data we want to share is a reducer function that will provide our common state and a dispatch function to allow us to update that state. We’ll also use a React hook called useReducer, which takes as an argument a function that implements the reducer pattern and provides some ease of use for accessing the reducer.

Changing our code to use a context and a reducer is a significant refactoring of our React code, so let’s outline what the data flow looks like:

  • Our existing data model sets a lot of data at the Venue level, which is passed down to each Row.
  • Each Row maintains a list of the statuses of its component seats and passes that information to each Seat.
  • The Seat status is held in the Row because the status depends on the status of neighboring seats and the Row is where all that data is stored.
  • When we click on a Seat, that data is passed back up to the Row, which updates the status and passes the status back down to the Seat.

If we add the subtotal calculator, which would be a sibling component to the rows, the situation gets even more complicated. The click event on a Seat would need to be passed back up to the Venue and then down to the subtotal calculator. Similarly, a click on the “Clear All” button would need to be passed up to the Venue and down to all the rows to clear all of the user’s seats.

Here’s a diagram:

Get hands-on with 1000+ tech skills courses.