Optimizing Redux Store Mapping
Posted on: 2017-12-05
Redux is a simple data flow that many React developers are using. Recently, Redux introduced the option to be able to shim a callback function when the Redux store send the notification that its state has changed. This is great because it allows us to perform logic between the store and to connected components. This possibility is so much more important if you are not keeping calculated values in the store and only normalized value as well. A good pattern is to denormalized the value into rich objects in the mapping, as well as performing logic on the data just before rending the object. However, this comes at a cost that can be saved if we knew that the information is not complete or that some crucial pieces are not fully loaded yet. A common scenario is that you are waiting for several Ajax call to finish and once all the data in the store to perform a visual update.
To be able to use the following code, you must have Redux version 5 and above. The Redux Connect API contains multiple options and the one we need is areStatesEqual
. The method is hidden under the forth parameters of connect
as a member of an option object.
Usually, you may have used connect the following way, but you can now use the latter one to have a custom function to evaluate the equality of the data.
// Before:
export default connect(mapStateToProps, mapDispatchToProps)(YourControl);
// After:
export default connect(
mapStateToProps,
mapDispatchToProps,
(stateProps, dispatchProps, ownProps) =>
Object.assign({}, ownProps, stateProps, dispatchProps),
{ pure: true, areStatesEqual: shouldMappingBeSkipped }
)(YourControl);
The third parameter is marked as optional in the documentation, but at this day, the newest definition file of Redux for TypeScript, make it required, hence here is the default merging code which merges the own props with the state and dispatch props. The fourth parameter is where you can insert your own logic. The first step is that you need to specify that the store is pure. The second is to define a function. The function has two parameters which are the previous Redux store and the next store. This lets you do a comparison to determine if you should or not allow the notification to re-render. You can also only rely on the next store to see if the data desired is present. As you see, the function is named "areStatesEqual" and not "shouldUpdate". So, if your function returns true, it means that the two states are equal, hence do not need to send the notification to update. It means that you must return true if you need to not send a signal to render and false if you want to send a signal to re-render.
You may draw a parallel between React and its "shouldComponentUpdate" method that allow skipping rendering depending on the previous and current properties. That is very similar. The major difference is that you can intercept it earlier in the data flow. The main advantage is performance improvement by not computing data from the store to the component if this one doesn't need it.
// Won't notify a change from the store to React if no active id, or no entity loaded, or that the one we would display is not present.
export function shouldMappingBeSkipped(
nextState: AppReduxState,
previous: AppReduxState
): boolean {
return (
nextState.entitiesX.activeId === undefined ||
Object.keys(nextState.entitiesX.all).length === 0 ||
nextState.entitiesX.all[nextState.entitiesX.activeId] === undefined
);
}
Recently, I have incorporated the areStateEqual
and I have seen a reduction of mapping between 30% to 75%. You can see a big gain if you have deep normalized objects or advanced filtering and sorting capabilities on grids. By avoiding to recompute for no reason, the application is more responsive.