Redux: Docs: Usage with React Router

Created on 27 Aug 2015  ·  61Comments  ·  Source: reduxjs/redux

People constantly get the impression we don't support React Router, or that you need something special like redux-react-router for it to work, or even that it doesn't work until React 0.14.

You can use Redux with React Router 0.13 or 1.0 as is today.
(And that was true since Redux's initial release by the way.)

Some features like time travel will need to wait for some support on RR side, but this is irrelevant to the main issue. People get confused thinking they can't use routing today, which is just wrong.

The real world example included in this repo uses React Router. All you need is to wrap <Router> into <Provider>, just like you'd wrap your top-level component in a router-less app.

If you want to transition from action creators, pass router instance as a parameter to action creators, and call its methods when you like. If you want to read router state from Redux store, fire an action on route change and write a reducer to handle it. That's it!

react-redux-router is an experiment in trying to come up with a more natural API where you'd just dispatch actions and read state from store which automagically is connected to the router instance, but you don't have to use it, or to wait for it to become stable! It's an experiment only.

We need this in docs..

docs

Most helpful comment

Also, how do you feel about adding an example that shows react router being universally used?

All 61 comments

I had originally wrote the server side rendering example to use react router in a universal context. It was suggested that usage with react router could become a document of its own. Perhaps we could add a section to the documentation?

Also, how do you feel about adding an example that shows react router being universally used?

I think modifying the real-world example to be universal would be a good idea. That way you don't have to build anything from scratch.

If you want to transition from action creators, pass router instance as a parameter to action creators

Just wanted to clarify, this only works in 0.13 as 1.0beta doesn't have a createRouter concept (yet?), correct?

You can do it with 1.0.0-beta3 from within your React components.

class Thing extends Component {
  static contextTypes = {
    router: PropTypes.object
  }

  handleThing() {
    this.props.actionCreator(this.context.router);
  }
}

@timdorr Is it safe to use this.context ? I was under the impression that it wasn't meant to be used externally.

Yes, it's just undocumented, not unsafe. It's getting changed slightly in 0.14, but not in a way that would break this. I imagine it will be documented at some point soon.

@timdorr Does that also mean it's possible to transition to a different url from an action creator in 1.0.0-beta3 ?

Yes, if you pass along the router instance to the action creator, you can do whatever you want with it, including transitions.

I threw together this:

const loginProps = {
  handleLogin: ({email, password}) => store.dispatch(userLogin({email, password})),
};



const routes = (
  <Route path="/" handler={App}>
    <Route path="login" handler={wrapper(Login, loginProps)} />
    <Route handler={authSection}>
      <Route path="" handler={Index} />
      <Route path="users" handler={wrapper(Users, (() => store.dispatch(getUsers())))} />
      <Route path="logout" handler={wrapper(Login, (() => store.dispatch(userLogout())))} />
    </Route>
  </Route>
);

const router = createRouter({
  location: HistoryLocation,
  routes,
});

store.dispatch(receiveRouter({router}));

Warning: Failed Context Types: Required context `store` was not specified in `SmartComponent(TodoApp)`. Check the render method of `Router`.

What's may be wrong?

P.S.: RR 1.0.0-beta3

@gyzerok Make sure you wrap the whole () => <Router>stuff</Router> into <Provider>, just like real-world example does it.

@gaearon yes, ofc. I wrap it not in Provider but in my own component which is mostly copy-paste of your Provider. The difference is I do not pass store to it, but create store inside of it.

@gyzerok It's hard to say what's wrong without seeing the code. (And please file a separate issue, react-redux repo is a good place.)

Thx for real-wolrd example! But how to deal with AsyncProps? Seems like double context manipulation won't work together.

import React from 'react';                                                                                                                                                                                         
import {createStore} from 'redux';                                                                                                                                                                                 
import {Provider} from 'react-redux';                                                                                                                                                                              
import {Router, Route} from 'react-router';                                                                                                                                                                        
import BrowserHistory from 'react-router/lib/BrowserHistory';                                                                                                                                                      
import AsyncProps from 'react-router/lib/experimental/AsyncProps';                                                                                                                                                 

import App from './containers/App';                                                                                                                                                                                
import reducers from './reducers';                                                                                                                                                                                 

const store = createStoreWithMiddleware(reducers);                                                                                                                                                                 
const history = new BrowserHistory();                                                                                                                                                                               

React.render(                                                                                                                                                                                                       
    <Provider store={store}>                                                                                                                                                                                        
        {() =>                                                                                                                                                                                                      
            <Router history={history} createElement={AsyncProps.createElement}>                                                                                                                                     
                <Route component={AsyncProps}>                                                                                                                                                                      
                    <Route path="/" component={App} />                                                                                                                                                              
                </Route>                                                                                                                                                                                            
            </Router>                                                                                                                                                                                               
        }                                                                                                                                                                                                           
    </Provider>,                                                                                                                                                                                                    
    document.body                                                                                                                                                                                                  
);

and App.js

import React from 'react';                                                                                                                                                                                         
import {connect} from 'react-redux';                                                                                                                                                                               

let App = React.createClass({                                                                                                                                                                                      
    statics: {                                                                                                                                                                                                     
        loadProps(params, cb) {                                                                                                                                                                                    
            // have to call this with AsyncProps                                                                                                                                                                   
        }                                                                                                                                                                                                          
    },                                                                                                                                                                                                             
    displayName: 'App',                                                                                                                                                                                            

    render() {                                                                                                                                                                                                     
        return <div children="this is app" />                                                                                                                                                                      
    }                                                                                                                                                                                                              
});                                                                                                                                                                                                                

export default connect(state => state)(App); 

It will work without connect wrapper, but there will no redux as well. Has anyone faced this problem?

Or may be any other way to pause navigation until data loaded?

Statics are just statics. You can put them on anything, including connect() result.

let App = React.createClass({                                                                                                                                                                                      
    displayName: 'App',                                                                                                                                                                                            

    render() {                                                                                                                                                                                                     
        return <div children="this is app" />                                                                                                                                                                      
    }                                                                                                                                                                                                              
});                                                                                                                                                                                                                

App = connect(state => state)(App); 

App.loadProps = function loadProps(params, cb) {                                                                                                                                                                                    
  // have to call this with AsyncProps                                                                                                                                                                   
}                                                                                                                                                                                                          

export default App; 

@gaearon sorry, example is not full. I tried extend connect result with missing static props, but real problem with context. Give me some time, I will push whole example into separate repo

Another thing I'm currently investigating is storing params in a redux store in a clear, unobstrusive way. After trying out few approaches, I ended up writing:

<Route
  component={OrderDetails}
  path='/orders/:orderId'
  onEnter={({params}) => store.dispatch(setCurrentOrder(params.orderId))} 
/>

so it's possible to refer to params from selectors, like below:

export const OrderDetails = state => {
  const {order} = state;
  return {
    order: order.details.get(order.currentOrderId),
    orderId: order.currentOrderId,
    isLoading: order.isLoadingDetails,
    error: order.detailsLoadingError
  };
};

It will probably change once more stable react-redux-router is released.

Good news: React Router 1.0 RC now exposes the hooks we need.
Check out https://github.com/acdlite/redux-react-router and let us know whether you like it now!

@gaearon i found the issue with react-router and experimental AsyncProps. Updating the react solves the problem

@wtfil Good to know!

When integrating with react-router, I understand from this thread that we can pass the router instance to an action creator and call methods on it. However, I also understand that action creators are intended not to have side effects in their purest form. The only case I see where action creators are allowed to have side effects is when they are asynchronous actions handled by middleware. Is the expectation then that action creators which perform transitions should be asynchronous, rather than pure functions?

Is the expectation then that action creators which perform transitions should be asynchronous, rather than pure functions?

Action creators are allowed to have side effects. It's best to avoid them when possible but of course at some point you need them, and since reducers are pure in Redux, and we don't have an explicit effect mechanism like in Elm (see #569 for discussion), action creators is the place to put them.

Check out redux-router. It works on top of React Router, but lets you dispatch actions and takes care of syncing the router.

waiting for React Router and Redux Router both hit 1.0...

Yep. We'll add a recipe after this happens.

So I've been following this discussion and also thinking a bit about how routing fits into redux in general (see https://github.com/rackt/redux/issues/805). Based on some discussion in that thread and some experimenting, I've found an approach that I personally prefer to the react-router/react-redux-router glue.

Basically, I try to not give react-router or redux any knowledge of each other and instead connect them via a custom history implementation. With this approach, routing is handled like this:

  1. An action, a reducer, and a key in the store are created for route.
  2. A standard history implementation (this example uses the preferred createHistory but you could easily use createHashHistory or whatever) is created and listened to. When the current location in the browser is changed, a ROUTE action is dispatched, ultimately putting the location to the store.
  3. A standard react-router instance is created with a second custom history implementation which notifies the subscriber (react-router) when the route key of the store is changed. It also defines createHref and pushState, delegating both to the standard history created in step 2.

That's it. I think it provides a fairly clean separation of duties between redux and react-router and removes the need to pull in another "glue" library. I'm pasting my code below, I'd be very interested in hearing any feedback:

// please pay attention to library versions, this strategy is only tested with the indicated versions
import React from 'react'; // v0.13.3
import { Provider } from 'react-redux'; // v3.1.0
import { Router, Route, IndexRoute, Link } from 'react-router'; // v1.0.0-rc3
import { createHistory } from 'history'; // v1.12.3
import { createStore } from 'redux'; // v3.0.2

// define some components
class About extends React.Component {
    render () {
        return (
            <div><h1>About</h1></div>
        )
    }
}
class Home extends React.Component {
    render () {
        return (
            <div>
                <h1>Home</h1>
                <Link to="/about">Go to about</Link>
            </div>
        )
    }
}

// create a standard history object
var history = createHistory();

// set up 'route' action and action creator
const ROUTE = 'ROUTE';
function createRouteAction (location) {
    return {
        type: ROUTE,
        payload: location
    };
}

// set up reducer. here we only define behavior for the route action
function reducer (state = {}, action) {
    if (action.type === ROUTE) {
        return Object.assign({}, state, {
            route: action.payload
        });
    }
    else {
        return state;
        // whatever other logic you need
    }
}

// create store
const store = createStore(reducer);

// this factory returns a history implementation which reads the current state
// from the redux store and delegates push state to a different history.
function createStoreHistory () {
    return {
        listen: function (callback) {
            // subscribe to the redux store. when `route` changes, notify the listener
            const unsubscribe = store.subscribe(function () {
                const route = store.getState().route;
                callback(route);
            });

            return unsubscribe;
        },
        createHref: history.createHref,
        pushState: history.pushState
    }
}

React.render(
    <Provider store={store}>
        {() =>
            <Router history={createStoreHistory()}>
                <Route path="/about" component={About} />
                <Route path="/" component={Home} />
            </Router>
        }
    </Provider>,
    document.getElementById('root') // or whatever
);

// when the url changes, dispatch a route action. this is placed at the bottom so that the first route triggers the initial render
const unlisten = history.listen(function (location) {
    store.dispatch(createRouteAction(location));
});

I mention this, by the way, because I think this example might help some people like myself who were struggling with the use of react-router with redux and help to clarify your assertion that redux and react-router can be used together today.

@cappslock I like it, there is one little thing though. In your implementation, changing route is the action, this kinda breaks "Action is an event not a command" approach which may eventually lead to some nasty practices. Let's invert the thinking, potentially any action can result in side effect which changes addressbar (either be that a component or real browser's addressbar....) and the side effect results in dispatching of new action (ROUTE_CHANGED). It's basically same pattern like triggering an API call.

@tomkis1 thanks for the feedback. If I understand you correctly, this actually already functions that way. The ROUTE action is dispatched as a side effect of the URL changing. Maybe ROUTE_CHANGED would be a better name?

Oh! I have just double checked and you were right. Yeah ROUTE_CHANGED would make better sense .

I think so too. I'd go back and change it but then these comments would be really confusing :)

The flow is like this, to clarify:

URL change -> ROUTE (or ROUTE_CHANGED) action -> reducer updates store -> store history (previously subscribed to store) notifies listeners -> react-router updates

I prefer this because I don't want anything but the store driving the user-observed state of the UI. It also seems preferable for testing.

A shortcoming of this approach is that the URL does not update in response to the ROUTE_CHANGED action. I am not even sure if this is desirable if we're saying we don't want actions to be treated as commands, but I imagine it could be completed either as a side effect of the ROUTE_CHANGED action creator or by a separate store subscriber.

BTW, if this discussion is beyond the scope of this issue, please let me know and I'll move it.

@cappslock I like that a lot! I definitely don't think it's a problem that dispatching ROUTE_CHANGED doesn't change the route. Treating the action as an event and not a trigger seems cleaner/more understandable to me (as it's responding to a user interaction; you would not expect a BUTTON_CLICKED action to actually trigger a click on a button). There's one part of your code that I don't get though. Can you elaborate on this bit for me?

this is placed at the bottom so that the first route triggers the initial render

@elliotdickison Thank you! I will try to clarify that, but please take what I say with a grain of salt as it's based on trial and error and assumptions rather than deep analysis. This is more of a proof of concept/sketch at this point.

When I had placed that code above the instantiation of ReactRouter, the component corresponding to the / route was not rendered. The router would still work if actions were manually dispatched or the state was manually pushed, so I figured it was a lifecycle issue with the history. Moving it below the instantiation of ReactRouter solved this. I suspect that the history library defers notification of the initial route until there is at least one subscriber. If that subscriber is set up before the ReactRouter is, none of the notifications reach it.

Attempting to explain this I realize my understanding is a bit lacking; I will look into this more and try to provide a better answer.

A shortcoming of this approach is that the URL does not update in response to the ROUTE_CHANGED action. I am not even sure if this is desirable if we're saying we don't want actions to be treated as commands, but I imagine it could be completed either as a side effect of the ROUTE_CHANGED action creator or by a separate store subscriber.

I'd say this is desired, ROUTE_CHANGED should definitely be fired by external source (eg onhashchange...) IMO URL change should result in ROUTE_CHANGED not vice versa.

I kind of agree. I thought it might be nice to just have something subscribing to the store and keeping the URL in sync in case a dispatched ROUTE_CHANGED action originated from code and not an actual history event, but you could argue that that case represents a programming error.

@cappslock your approach is really really good. Could you make some blogpost about it?

@vojtatranta Just check out https://github.com/rackt/redux/issues/805 I guess it was the inspiration behind the implementation.

@vojtatranta Thanks! I don't have a blog, so pretty much all the information I have is in this thread and #805. Anything in particular you wanted more information on?

1.0 is out.

It's time to:

  • Port Routing example to using it
  • Add “Usage with Router” recipe based on real-world example

:clap:

@gaearon can you reference this issue when the _Usage with Router_ example is in a PR? Many folks I know (myself included) are looking for clarification on how these two play well together.

Yes, sure. This is when the issue will get closed. :-)

Maybe redux-simple-router should be considered now?

redux-simple-router +1

I just converted the universal example + react-router (+redux-simple-router)
https://github.com/eriknyk/redux-universal-app

hi all, what's the conclusion of this discussion? i see the docs are not updated for usage with react-router
cc @gaearon

@gaearon after i bound my react application to redux, i just use state to control the display/hidden of my components. So I don't think the original "router" role like RR fits to my application now.
The only thing i think the "new router" need to do is map the url to state(via actions?) and remap the state back to url too.
If we let url decide how the application component(maybe some of them) be displayed, that means we have two sources of state, one is the url, the other one is redux's store, that will make things harder...
What do you say about this? should we just let the address bar to be another of component of our application.

Thanks

I officially commit to writing this doc after we ship https://github.com/rackt/react-router-redux/pull/259. This will be the blessed way of binding React Router and Redux. In the doc, we will first show how to use them without that package, and gradually introduce two conveniences that the package affords: middleware and moving routing source of truth into the store. Both are optional so we will make sure to explain in which case you want to opt-in to using them, and what they give you over vanilla RR.

Here is a thought to consider: if anything you explain about middleware related to routing and history could also become a specific chunk of realistic application within the general explanation of http://rackt.org/redux/docs/advanced/Middleware.html (for example, in the examples at the end)

@gaearon any progress on the React Router doc / next steps? I'm reading through the Redux documentation and loving it, but bummed out by those fake links :(

I only just started rewriting react router docs in my personal repo and I'm planning to have the redux section there as well. I might have something done soon, depends on my free time. I will keep you updated. https://github.com/knowbody/react-router-docs

To be fair you don't need anything on Redux side to get react router working. But yeah we need to do this.

Heads up: React Router 3.0 will work better with React Redux connect() optimizations, and new withRouter() HOC means you don’t need to use context directly.

https://twitter.com/dan_abramov/status/729768048417251328

@gaearon, @timdorr could you clarify the trade-offs between passing a router instance as an argument to action creators and directly importing browserHistory in the action creators (as suggested here https://github.com/reactjs/react-router/blob/master/docs/guides/NavigatingOutsideOfComponents.md?

router just wraps your history instance with some extra goodies on it, but it's the same push and replace methods between the two instances.

Thanks, @timdorr.

I guess the question then becomes why do we need the withRouter() composition if router basically wraps the history singleton (it's a singleton, right)?

Is it to allow looser coupling between a component and a history instance (i.e. to prevent the component from accessing directly a singleton object)? If so, wouldn't the same logic apply when accessing the history instance from an action creator?

Yes, and if you provide your own history instance and don't want to (or can't) make your own singleton module (which can be confusing if you're not super-familiar with JS module systems). If you want to do that yourself, you're more than welcome to follow our pattern.

I am not sure if it would be worth-while to document withRouter on how it can be used for multiple higher order components. I am still trying to figure out the best way to avoid this:
connect(mapStateToProps, mapDispatchToProps)(withRouter(withAnalytics(withLanguage(TestForm))));.

I could also use something like compose?

const enhance = compose(
  connect(mapStateToProps, mapDispatchToProps),
  withRouter,
  withAnalytics,
  withLanguage
);

export default enhance(TestForm);

However, my use case the context is going to have logged in user, current language, theme information, and analytics which makes this challenging with lots of context and a lot of connected components.

Another idea is to duplicate withRouter and connect logic under one context namespace: withAppContext() => props.app = { user, lang, theme, analytics, router, connect? }?

Would this be beneficial for the documentation or an example withRouter usage with connect?

@gaearon are there any updates for this now that React Router 3.0.0 and your new Egghead videos have been out for a while, and this thread has been opened for a year?

Done in #1929

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jbri7357 picture jbri7357  ·  3Comments

vslinko picture vslinko  ·  3Comments

rui-ktei picture rui-ktei  ·  3Comments

cloudfroster picture cloudfroster  ·  3Comments

olalonde picture olalonde  ·  3Comments