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.
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:
pets
: A list of pets in the pet store, which we likely got from a fetch to the server.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.
So far we have 3 relevant pieces of information:
pets
: The list of petstheme
: The appearance themenumPets
: The number of petsWe 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.
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 |
|
|
Cons |
|
|
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.
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.