Skip to content
This repository was archived by the owner on Oct 26, 2018. It is now read-only.
This repository was archived by the owner on Oct 26, 2018. It is now read-only.

Alternative Proposed Solution For React-Router + Redux Integration #248

Closed
@mjrussell

Description

@mjrussell

(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:

  1. 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.

  2. 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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions