This is probably the single most important tip in this repository. From my experience, the vast majority of crippling performance problems can be traced back to an incorrect implementation of mapStateToProps()
.
Be aware the mapStateToProps()
is called on EVERY store change. This can lead to wasted rerenders when completely unrelated parts of the application update.
Consider the following:
const mapStateToProps = state => ({
activeFoos: state.foos.filter(foo => foo.isActive),
});
filter()
creates a new array every time it is called. connect()
is called whenever an action dispatch occurs. Thus, every single time the Redux store receives an action, the component connected via the above function will rerender due to a prop change.
Fix:
import { createSelector } from 'reselect';
const getActiveFoos = createSelector(
state => state.foos,
foos => foos.filter(foo => foo.isActive),
);
const mapStateToProps = state => ({
activeFoos: getActiveFoos(state),
});
Similarly, this leads to the same problem:
const mapStateToProps = (state, props) => ({
activeFoos: props.foos || [],
});
The default []
value changes on each call to mapStateToProps()
which leads to a detected prop change whenever the store is updated.
Fix:
const EMPTY_FOOS = [];
const mapStateToProps = (state, props) => ({
activefoos: props.foos || EMPTY_FOOS,
});
Sometimes it is tempting to do this:
const mapStateToProps = (state, props) => ({
// ...stuff...
state,
});
This lets the component use the state object flexibly, which can SEEM to alleviate the boilerplate needed from selectors, etc. However, this also means the component will re-render on EVERY state change, even to parts of the state that are entirely unrelated to the component in question.
The implementation of connect
actually checks the arity of your mapStateToProps
function, and does an optimization if only one argument is accepted:
// A length of one signals that mapToProps does not depend on props from the parent component.
// A length of zero is assumed to mean mapToProps is getting args via arguments or ...args and
// therefore not reporting its length accurately..
export function getDependsOnOwnProps(mapToProps) {
return (mapToProps.dependsOnOwnProps !== null && mapToProps.dependsOnOwnProps !== undefined)
? Boolean(mapToProps.dependsOnOwnProps)
: mapToProps.length !== 1
}
This means that if your mapStateToProps
is written to take arguments (state, props)
, it will be recomputed when props change, even if you do not use props in the function. This also means that if you use arguments
in your function to access props for whatever reason (please don't do this), your function will not recompute when props change, leading to errors.
Normally, this shouldn't be impactful, because mapStateToProps
should be implemented to be cheap to compute. However, when coupled with incorrectly cached selectors or other expensive computations, this can degrade performance by causing unnecessary rerenders.