The usual pattern for rendering lists of components often ends with delegating all of the responsibilities of each child component to the entire list container component. But with a few optimizations, we can make a change in a child component not cause the parent component to re-render.
Let's make a list of React components go from this:
To this:
To be clear, the first example above doesn't actually cause wasted re-renders, according to react-addons-perf
.
We'll be using TypeScript (for clarity and clearly defining what data our components receive) and Immutable.js. The source code of the finished app is available on github and a live demo is available here.
The problem
Let's imagine that we're building a website to list animals for adoption, and that each animal looks like this:
export interface Animal {
readonly id: string;
readonly name: string;
readonly adopted: boolean;
readonly type: 'Cat' | 'Dog';
}
Consider this react-redux
implementation of rendering a list of DOM elements to display animals for adoption. The important stuff is in the Props
and Handlers
interfaces.
We get most of what we need to know from the Props
and Handlers
interfaces: AnimalTable accepts an immutable map of Animals. We see in the mapState
function that it gets that data from subscribing to the store's animal
property. It also receives the dispatching function to toggle an animal's adopted
property (which we'll just trust is implemented properly, since it doesn't matter for this example).
In AnimalTable's render
method, for each of the animals in the Animal Map it renders an AnimalListing, passing it the toggleStatus
action creator and an Animal. We can guess that an AnimalListing will contain a button to invoke toggleStatus
and will display the information specific to its animal.
To summarize, AnimalTable does a lot. Without even looking at the code for AnimalListing we can guess at everything it does because AnimalTable is responsible for all of the functionality of each AnimalListing. AnimalTable doesn't even do anything with the callback its passed in, it only passes it along to each AnimalListing. And does it really need the entire map of animals, complete with the entire Animal object?
We can simplify this massively. Each AnimalListing can be a connected react-redux component that subscribes only to one particular animal. But how do we know which animal to give to which AnimalListing? The key is in the second, optional argument that can be passed in to the first argument of connect
(which is idiomatically called mapStateToProps
). The second argument of mapStateToProps
are the props that the parent of a component passes down to a connected component, which we can call OwnProps
.
Connecting individual AnimalListings
If AnimalTable passes down just an id to each AnimalListing, AnimalListing could use that id to subscribe to a specific animal in the store. Let's look at an AnimalListing that implements this pattern:
The important part here are the Props
and OwnProps
interfaces, and the mapState
function. When mapState
is called on an AnimalListing, AnimalListing already has the id that AnimalTable gives it, and we use that id to assign it a specific Animal.
Also notice that we were able to move the action that dispatches toggleAdoption
to the component that actually uses it, AnimalListing.
Fixing AnimalTable
With this pattern, AnimalTable really only needs an array of ids, so it could look like this:
Now AnimalTable receives just an immutable List of strings, which are animalIds it passes down to each AnimalListing. The only problem here is that we're doing a lot of logic in our mapState
function, which we'll solve in the next section.
Adding reselect
To hide the logic we're now implementing in AnimalTable's mapState
function, we can use the ultra-light reselect
library.
// src/selectors.ts
import { createSelector } from 'reselect';
import { RootState, Animal } from './types';
const animalsSelector = (state: RootState) => state.animals;
export const animalIdsSelector = createSelector(
[ animalsSelector ],
(animals) => animals.map((animal: Animal) => animal.id).toList()
);
And now we can use it in AnimalTable's mapState
:
import { animalIdsSelector } from '../selectors';
...
const mapState = (state: RootState): Props => ({
animalIds: animalIdsSelector(state)
});
...
Although it isn't doing much in this example, reselect is a valuable tool when you start sorting, filtering, and otherwise manipulating data before you pass it to a component (here's an example from one of my own projects). And if a change in one selector doesn't change the result of another, your component won't re-render.
The final touch
At this point we have implemented everything we need to leverage immutable data structures to prevent re-rendering the entire AnimalTable when we adopt a single animal: a custom shouldComponentUpdate
.
class AnimalTable extends React.Component<Props, never> {
shouldComponentUpdate(nextProps: Props) {
return !nextProps.animalIds.equals(this.props.animalIds);
}
...
shouldComponentUpdate
will return false if the props its receiving are equal to the props it already has. And because the AnimalTable is receiving just a List of string IDs, a change in the adoption status won't cause AnimalTable to receive a different set of IDs.