Alternative Proposed Solution For React-Router + Redux Integration #248
Description
(tl;dr - maybe we should use the RouterContext
from React-Router v2 for syncing the redux-store? We could get routing-connected components without infinite loops and router props in the store!)
For starters, I've been really impressed with goals and results that this project has displayed in such a short time. Moving from syncing the pathname to the entire location descriptor as well as improving the support for the devtools while keeping the API super simple and close to React-Router has been a big win for both the users and the maintainers (or at least it appears to be from my end).
Tying to history directly as the source for updating the store and vice-versa makes this project very flexible (you don't even need React-Router technically), but I think results in some negative consequences:
-
Connecting to the Routing State is Dangerous
With syncing with history, you must discourage users from doing what seems natural in a redux setting and connecting the router state to their components (Infinite loop when dispatching redirect from within a componentWillReceiveProps #212) leading to the following in the README:
Warning: It is a bad pattern to use react-redux's connect decorator to map the state from this reducer to props on your Route components. This can lead to infinite loops and performance problems. react-router already provides this for you via this.props.location.
I think this is due to the fact that the router props and the redux-store are not from the same source (only indirectly via history) and therefore are not guaranteed to update in sync.
-
Store omits the React-Router Props
The second limitation of the history-redux approach is missing out on React-Router's params in the redux -store, the source of confusion and many feature requests (passing params from react-router #246, Could use fuller explanation of project vision #228, etc)
I know that you can use the props directly from React-Router, and I've responded to several issues and discord questions with that very advice. I would be happy with these tradeoffs for simplicity if there were not another solution.
An Alternative using RouterContext
I think that React-Router 2.x actually provides a best of both worlds approach where we can get an easy API which matches React-Router, maintains a small codebase, and solves the two limitations above. I actually stumbled across this while trying to see if there was a way to upgrade the old redux-router (before this project became part of rackt and added the location object sync).
Here's an alternative using the RouterContext
from React-Router 2. The central idea is that the props passed to the RouterContext
are directly from the store, therefore a component below this ReduxRouter
cannot ever get a property from the router that is different than the store's data. The ReduxRouterContext
is responsible for managing and resolving differences between the store and the props passed down from ReactRouter
function routerPropsFromProps(props) {
return {
location: props.location,
params: props.params,
};
}
@connect(
(state, { routerStateSelector }) => { return { routerState: routerStateSelector(state) }; },
{ routerDidChange }
)
class ReduxRouterContext extends Component {
static propTypes = {
router: PropTypes.object,
routerDidChange: PropTypes.func.isRequired,
routerState: PropTypes.object,
}
componentWillMount() {
// On mount, sync to the store to React-Router props
this.props.routerDidChange(routerPropsFromProps(this.props));
}
componentWillReceiveProps(newProps) {
// New routing props from React-Router and it doesnt match our store, Update the store
if (!routerStateEquals(routerPropsFromProps(newProps), routerPropsFromProps(this.props)) &&
!routerStateEquals(routerPropsFromProps(newProps), newProps.routerState)) {
this.props.routerDidChange(routerPropsFromProps(newProps));
// New store state and it doesnt match the next routing props, transition via the router
// This is common when replaying devTools
} else if (!routerStateEquals(newProps.routerState, this.props.routerState) &&
!routerStateEquals(routerPropsFromProps(newProps), newProps.routerState)) {
if (newProps.routerState) {
newProps.router.transitionTo(newProps.routerState.location);
}
}
}
render() {
const { routerDidChange, routerState, ...others } = this.props;
const newRouterProps = {
...others,
...routerState, // Override react-router's props with the store routing props
};
return <RouterContext {...newRouterProps} />;
}
}
class ReduxRouter extends Component {
render() {
return (<Router render={props => <ReduxRouterContext {...props} />}
{...this.props}
/>);
}
}
I omitted a small wrapper class which facilitates in memoization and passing down the state selector, as well as the routerStateEquals which would compare location objects (maybe omit the key if users dont want a re-render on same page loads #222?). You would also add a small middleware for history so users can do the routeActions.push
and other history navigation from redux which would just call history directly. Also of course would be a reducer to store the data from routerDidChange
.
The use for this would be a (really) minor tweak to how users interact with the current React-Router API.
Basically anywhere a user would use <Router>
they use <ReduxRouter>
(or ReactRouterRedux 😄 ) or if server rendering, use <ReduxRouterContext>
instead of <RouterContext>
.
Feel free to peruse the actual source at https://github.com/mjrussell/redux-router/tree/rr-v2 with tests and the examples (basic and server rendering) originally from redux-router updated.
The Way Forward
I'd love for this (way too long) post to be a discussion about the tradeoffs of an approach like this compared to the current implementation/direction. Maybe this implementation is completely flawed and should be thrown out, or there is a similar approach but with some tweaks. Or, this is what Redux-Router should evolve into and then maintain a 2 redux-routing approach in the ecosystem.
Thanks and keep up all the great work!