Workarounds for mapStateToProps getting called with new route immediately before unmount
See original GitHub issueI just put this here in case anyone else runs into it and I think something like this should be in the docs.
Anyone working with Redux should know that mapStateToProps
gets called whenever the state is changed. However, it’s easy to forget that each redux-first-router action will change the state and that a components mapStateToProps
will be called when arriving at a route and also again when navigating away from that route (…immediately before the current component is unmounted, causing an unnecessary re-render). This can be problematic when you have a component-per-page and expect the RFR payload and route type to have a certain value in your mapStateToProps
implementation.
UPDATE: My new workaround for this is to memoize the entire return value of mapStateToProps
so that a cached return value is returned whenever your navigation route changes. You could probably also use something like reselect for this, with a custom equality checker (but this is simpler).
/**
* Returns a memoized value, so that the given mapToProps function
* is only called for routes that it's concerned with. A component
* using this selector will not re-render immediately before being
* unmounted upon navigation to a different page.
*/
function mapToPropsForPage(pageType, mapToProps) {
let lastReturnValue;
return function mapPropsForPage(state) {
if (state.location.type !== pageType) {
return lastReturnValue;
}
lastReturnValue = mapToProps(state);
return lastReturnValue;
};
}
// Example usage:
const mapStateToProps = mapToPropsForPage(
'PAGE_XYZ',
(state/*, props */) => {
// This function will only run when the state.location.type === 'PAGE_XYZ'
return {
id: state.location.payload.id,
otherThings: state.other.things,
};
},
);
NOT RECOMMENDED: Another workaround for this is to return undefined from mapStateToProps
instead of memoizing the value. I created a function that only calls the target mapStateToProps
when the current state.location.type
matches a given value. A simplified version of it is show below.
To use it, you just wrap your own mapStateToProps
with filterState
, passing filterRoute('PAGE_XYZ')
as the first argument. However, I don’t recommend this method anymore since it actually causes all previously mapped properties to be changed to undefined.
/**
* Returns a function that only calls the given mapStateToProps when stateFilter returns true.
* @param {(state)=> boolean} stateFilter
* @param {(state: object)=> object} mapStateToProps
*/
function filterState(stateFilter, mapStateToProps) {
return function mapStateToPropsFilter(state) {
if (!stateFilter(state)) {
return undefined; // This is perfectly fine with Redux, as a way to early-exit mapStateToProps.
}
return mapStateToProps(state);
}
}
/**
* Returns a function that returns false whenever state.location.type is not equal to the page type.
* @param {string} type
*/
function filterRoute(type) {
return function filterRoutePageType(state) {
if (!state) {
return false;
}
const { location } = state;
if (!location) {
return false;
}
return location.type === type;
}
}
Issue Analytics
- State:
- Created 6 years ago
- Reactions:2
- Comments:10 (4 by maintainers)
Top GitHub Comments
Just to state the obvious: This is only a problem if you structured your app like me - where you have one component per page type. The solution could also be expanded if you have one component that handles multiple page types.
However, if you have only one
mapStateToProps
in your whole app, which handles every type of route - then this tactic shouldn’t be necessary.Here’s a scenario that happened to me with the undefined state:
/:orgId/clients/
calledOrgClientsPage
.OrgClientsPage.mapStateToProps
expectsstate.location.payload.orgId
to exist, which works just fine the first time its called./settings/
, which has noorgId
in its payload.OrgClientsPage.mapStateToProps
is then called again, right before navigating away from the page, but this time thelocation.state.payload.orgId
doesn’t exist.The other problem that can happen is if you navigate to another page which happens to have an
/:orgId
in its payload, despite it not being the same page type at all.The only way to ensure that
mapStateToProps
will not run with incorrect payload and query inputs is to filter calls to it by looking at thestate.location.type
first.