Where state goes: React + Redux

2022 August 18

With React and Redux, you should put state in the Redux store and leave everything else outside of it. Let’s explore why.

If you’re not familiar with React and/or Redux, here’s a summary highlighting the parts of them that are important to this post.

React is “a Javascript library for building user interfaces.” It offers a declarative interface for the programmer to say what should be on the page. A React “component” is the basic building block of the UI. At its heart a component is just a function that “reacts” to different state configurations by saying what should be displayed given that configuration. Because these functions say what should be displayed (this is what we mean by “declarative”), they can be and often are run multiple times by the overall framework. If the same result is returned, the framework does not make a change to the UI.

Redux is “a Predictable State Container for JS Apps”. Redux offers a clean ways to store and manage application state. Redux usage consists of setting up a central Redux “store” where your state goes, defining rules for how that store can change, and connecting that store to anything that may need to use the state. It pairs well with React - Redux manages the state, and React connects to that state and determines what should be displayed.

A pet store’s website

Our example application will have a central Redux state model, like this:

interface PetStoreState {
  pets: Pet[],
  theme: 'light' | 'dark'
}

const initialState: PetStoreState = {
  pets: [],
  theme: 'light'
}

Here our Redux store manages two pieces of information:

  1. pets: A list of pets in the pet store, which we likely got from a fetch to the server.
  2. theme: A simple user preference for whether to display the page in light or dark mode, which let’s say we got from prompting the user when they first navigated to the page.

Let’s say that as part of our render layer we want to display a page header that shows the number of pets in the pet store. We’ll have a React component use a Redux selector to grab the relevant state it needs to display that number, like this:

const PageHeader = () => {
  const numPets = useSelector((state) => state.pets.length);
  return <h1>Browse pets ({numPets})</h1>;
}

This React component connects to the Redux store and computes some derived information: the number of elements in the pets array. This computation (accessing the .length field of the state.pets array) is dead simple and extremely fast by design for the sake of this example, but keep in mind that it represents some computation.

As we said before, this React component’s function may be run multiple times in order for the React framework to ensure what’s on screen is up-to-date, but as long as the length of the pets array remains the same the component tells React the same answer for how the UI should look.

Where should these pieces of information live?

So far we have 3 relevant pieces of information:

  • pets: The list of pets
  • theme: The appearance theme
  • numPets: The number of pets

We chose to store the first two in Redux and put the last one in a React component to be computed every time the React component renders.

For argument’s sake, here’s an alternative way we could have structured the state and the component:

interface PetStoreState {
  pets: Pet[],
  numPets: number,
  theme: 'light' | 'dark'
}

const PageHeader = () => {
  const numPets = useSelector((state) => state.numPets); // a tiny bit simpler
  return <h1>Browse pets ({numPets})</h1>;
}

We’ve taken the last piece of information, the number of pets in the pet store, and moved it from being computed directly in the component where it’s used to being stored in the Redux state.

If we had it this way, the computation that the component does is a little simpler. When the developer is building the component they just have to access a ready-made field in the state rather than know how to / write the computation to determine the length of the array (again, trivial I know, but it’s for the sake of example). And when React is running this function, however many times it will, it will save some (…trivial) amount of compute on each run.

However, our Redux store is now that little bit more complex. Now we have to make sure that the numPets field stays in sync with the pets field whenever the pets field changes. If we had an action in our reducer to add a pet to the store, we’d have to now consider numPets:

interface AddPetAction {
  type: 'ADD_PET',
  pet: Pet
}

function petStoreReducer(state: PetStoreState, action): PetStoreState {
  ...
  if (action.type === 'ADD_PET') {
    return {
      ...state,
      pets: [...state.pets, action.pet],
      numPets: state.pets.length + 1, // we didn't need this consideration before
    };
  }
  ...
}

Turns out, this is all a violation of Redux’s best practices - Redux tells us to keep the store as minimal as possible, and put any data that we can derive from what’s in the store outside of the store. Putting the number of pets here complicates the store, where complexity has a higher weight and severity, and leads to more places the developer needs to keep that data in sync with the other data in the store.

So Redux would tell us it’s best to keep numPets being computed on the fly, directly within the component it’s used in, instead of stored in the Redux store.

Why?

We’ve covered that it saves some developer thought (how to compute numPets) when writing the component, and that it saves React some compute when running the component function. But it requires more cognitive load to understand the Redux store, and more manual effort from the developer to keep the store in sync with itself.

This manual effort to keep the store in sync with itself is worse than it looks. Putting numPets in the store actually opens up the possibility for new bugs due to impossible states. It is now possible to have the Redux store be in a state where the application has, say, 5 pets in the pets list, but the value 4 in the numPets field. It’s now possible for the store to look like this:

const store: PetStoreState = {
  pets: [pet1, pet2, pet3, pet4, pet5],
  numPets: 4, // impossible state!
  theme: 'light'
}

The increased manual effort for the developer to keep the store in sync with itself not only means the developer’s life is a little tougher, but also that they could skip this effort:

function petStoreReducer(state: PetStoreState, action): PetStoreState {
  ...
  if (action.type === 'ADD_PET') {
    return {
      ...state,
      pets: [...state.pets, action.pet]
      // where's numPets?
    };
  }
  ...
}

Or make a mistake:

function petStoreReducer(state: PetStoreState, action): PetStoreState {
  ...
  if (action.type === 'ADD_PET') {
    return {
      ...state,
      pets: [...state.pets, action.pet],
      numPets: state.pets.length // whoopsies
    };
  }
  ...
}

The beauty of keeping the computation in the component is that the machine will automatically keep these pieces of information in sync for us. When the state changes React will re-run the component’s function, which will derive the length of the new pets array. This is done by the machine whenever the state changes, which is exactly when we’d have to consider whether or not to update numPets manually in the stored-in-the-store scenario.

Summarizing what we’ve said, we get a pros/cons table:

Computed in component Stored in store
Pros
  • No impossible states
  • Redux store is less complex
  • Lower compute cost (avoid component rerender costs)
  • Easier to write React component
Cons
  • Higher compute cost every time component is rendered
  • Harder to write React component
  • Redux store is more complex
  • Developer has to address possibility of impossible states

Regarding it being easier / harder to write the component – in both scenarios, some developer needs to know how to perform the computation of getting the length of the array. Only in the stored-in-the-store scenario does the question of where to perform the computation become complicated and potentially error-prone. So that benefit cancels out in the cost to keep the store in sync with itself.

Regarding the compute cost – this computation is extremely fast. We are talking about absolutely negligible amounts of compute. Nobody from the developer to the end user would possibly detect the difference. So we should favor other larger benefits over this.

The overall “why” for the question of which is better here boils down to this: making the developer’s job easier is extremely valuable. Making the component easier to write doesn’t offset the difficulty of keeping the store in sync, and the pros / cons regarding compute costs fall far by the wayside of the importance of making the store more understandable and avoiding impossible states. Higher understandability of the code and fewer impossible states to consider translate into value for the end-user in the form of fewer bugs and faster development. A lot of senior developers spend a lot of time considering understandability of the code and the possibility of impossible states, whether they’re working with React / Redux or not, for exactly this reason.

Getting more complicated

This example is certainly contrived in that the tradeoff is very, very obvious - the machine time it takes to compute the number of elements in a list is absurdly low, and the number of times a React component renders doesn’t provide a large enough multiple to that cost for the performance benefit from avoiding those computations to hold much weight.

The temptation might be easier to see if we make the computation more complex. Like what if we had 1 million elements in the pets array, and the page header needed to display the number of pets of a certain breed.

const PageHeader = () => {
  const numPoodles = useSelector((state) =>
    state.pets.filter(p => p.breed === 'poodle').length
  );
  return <h1>Browse poodles ({numPoodles})</h1>;
}

Maybe this might start taking some real time to compute, and maybe it might start to look more tempting to store the number of poodles in the Redux store. The answer is still not to confuse your derived state with your essential state. The answer would be to use memoized selectors, but I’ll talk about that in another post.