Redux: firing actions in response to route transitions in react-router

Created on 7 Jul 2015  ·  26Comments  ·  Source: reduxjs/redux

Hi Guys,

I am using react-router and redux in my latest app and I'm facing a couple of issues relating to state changes required based on the current url params and queries.

Basically I have a component that needs to update it's state every time the url changes. State is being passed in through props by redux with the decorator like so

 @connect(state => ({
   campaigngroups: state.jobresults.campaigngroups,
   error: state.jobresults.error,
   loading: state.jobresults.loading
 }))

At the moment I am using the componentWillReceiveProps lifecycle method to respond to the url changes coming from react-router since react-router will pass new props to the handler when the url changes in this.props.params and this.props.query - the main issue with this approach is that I am firing an action in this method to update the state - which then goes and passes new props the component which will trigger the same lifecycle method again - so basically creating an endless loop, currently I am setting a state variable to stop this from happening.

  componentWillReceiveProps(nextProps) {
    if (this.state.shouldupdate) {
      let { slug } = nextProps.params;
      let { citizenships, discipline, workright, location } = nextProps.query;
      const params = { slug, discipline, workright, location };
      let filters = this._getFilters(params);
      // set the state accroding to the filters in the url
      this._setState(params);
      // trigger the action to refill the stores
      this.actions.loadCampaignGroups(filters);
    }
  }

Is there a standard approach to trigger actions base on route transitions OR can I have the state of the store directly connected to the state of the component instead of passing it in through props? I have tried to use willTransitionTo static method but I don't have access to the this.props.dispatch there.

docs ecosystem question

Most helpful comment

@deowk There are two parts to this problem, I'd say. The first is that componentWillReceiveProps() is not an ideal way for responding to state changes — mostly because it forces you to think imperatively, instead of reactively like we do with Redux. The solution is to store your current router information (location, params, query) _inside_ your store. Then all your state is in the same place, and you can subscribe to it using the same Redux API as the rest of your data.

The trick is to create an action type that fires whenever the router location changes. This is easy in the upcoming 1.0 version of React Router:

// routeLocationDidUpdate() is an action creator
// Only call it from here, nowhere else
BrowserHistory.listen(location => dispatch(routeLocationDidUpdate(location)));

Now your store state will always be in sync with the router state. That fixes the need to manually react to query param changes and setState() in your component above — just use Redux's Connector.

<Connector select={state => ({ filter: getFilters(store.router.params) })} />

The second part of the problem is you need a way to react to Redux state changes outside of the view layer, say to fire an action in response to a route change. You can continue to use componentWillReceiveProps for simple cases like the one you describe, if you wish.

For anything more complicated, though, I recommending using RxJS if you're open to it. This is exactly what observables are designed for — reactive data flow.

To do this in Redux, first create an observable sequence of store states. You can do this using redux-rx's observableFromStore().

import { observableFromStore } from 'redux-rx';
const state$ = observableFromStore(store);

Then it's just a matter of using observable operators to subscribe to specific state changes. Here's an example of re-directing from a login page after a successful login:

const didLogin$ = state$
  .distinctUntilChanged(state => !state.loggedIn && state.router.path === '/login')
  .filter(state => state.loggedIn && state.router.path === '/login');

didLogin$.subscribe({
   router.transitionTo('/success');
});

This implementation is much simpler that the same functionality using imperative patterns like componentDidReceiveProps().

Hope that helps!

All 26 comments

@deowk You could compare this.props with nextProps to see if the relevant data changed. If it didn't change, then you don't have to trigger the action again and can escape the infinite loop.

@johanneslumpe: In my case campaigngroups is a rather large collection of objects, it seem kind of inefficient use a deep object comparison as a condition in this way?

@deowk if you use immutable data, you can just compare references

@deowk There are two parts to this problem, I'd say. The first is that componentWillReceiveProps() is not an ideal way for responding to state changes — mostly because it forces you to think imperatively, instead of reactively like we do with Redux. The solution is to store your current router information (location, params, query) _inside_ your store. Then all your state is in the same place, and you can subscribe to it using the same Redux API as the rest of your data.

The trick is to create an action type that fires whenever the router location changes. This is easy in the upcoming 1.0 version of React Router:

// routeLocationDidUpdate() is an action creator
// Only call it from here, nowhere else
BrowserHistory.listen(location => dispatch(routeLocationDidUpdate(location)));

Now your store state will always be in sync with the router state. That fixes the need to manually react to query param changes and setState() in your component above — just use Redux's Connector.

<Connector select={state => ({ filter: getFilters(store.router.params) })} />

The second part of the problem is you need a way to react to Redux state changes outside of the view layer, say to fire an action in response to a route change. You can continue to use componentWillReceiveProps for simple cases like the one you describe, if you wish.

For anything more complicated, though, I recommending using RxJS if you're open to it. This is exactly what observables are designed for — reactive data flow.

To do this in Redux, first create an observable sequence of store states. You can do this using redux-rx's observableFromStore().

import { observableFromStore } from 'redux-rx';
const state$ = observableFromStore(store);

Then it's just a matter of using observable operators to subscribe to specific state changes. Here's an example of re-directing from a login page after a successful login:

const didLogin$ = state$
  .distinctUntilChanged(state => !state.loggedIn && state.router.path === '/login')
  .filter(state => state.loggedIn && state.router.path === '/login');

didLogin$.subscribe({
   router.transitionTo('/success');
});

This implementation is much simpler that the same functionality using imperative patterns like componentDidReceiveProps().

Hope that helps!

We need this in new docs. So well written.

@acdlite: Thank you very much, your advise really helped me out a lot, I am struggling with the following though --> changing the state in the store by triggering an action creator in response to a url change.

I want to trigger the action creator in the willTransitionTo static method since this seems like the only option in react-router v0.13.3

class Results extends Component  {
..........
}

Results.willTransitionTo = function (transition, params, query) {
 // how do I get a reference to dispatch here?
};

@deowk My guess is that you call dispatch directly on the redux instance:

const redux = createRedux(stores);
BrowserHistory.listen(location => redux.dispatch(routeLocationDidUpdate(location)));

@deowk I currently dispatch my url changes in react router 0.13.3 like this:

Router.run(routes, Router.HistoryLocation, function(Handler, locationState) {
   dispatch(routeLocationDidUpdate(locationState))
   React.render(<Handler/>, appEl);
});

@acdlite really well explained! :+1:
Agree it should be in the new docs as well :)

Just as a note, BrowserHistory.history() has changed to BrowserHistory.addChangeListener().

https://github.com/rackt/react-router/blob/master/modules/BrowserHistory.js#L57

EDIT:

Which might look like this:

history.addChangeListener(() => store.dispatch(routeLocationDidUpdate(location)));

And what about the initial state for that reducer, @pburtchaell?

I have the following setup:

// client.js
class App extends Component {
  constructor(props) {
    super(props);
    this.history = new HashHistory();
  }

  render() {
    return (
      <Provider store={store}>
         {renderRoutes.bind(null, this.history)}
      </Provider>
    );
  }
}

// routes.js
export default function renderRoutes(history) {

  // When the route changes, dispatch that information to the store.
  history.addChangeListener(() => store.dispatch(routeLocationDidUpdate(location)));

  return (
    <Router history={history}>
      <Route component={myAppView}>
        <Route path="/" component={myHomeView}  />
      </Route>
    </Router>
  );
};

This fires the routeLocationDidUpdate() action creator both each time the route changes and when the app initially loads.

@pburtchaell can you share your routeLocationDidUpdate?

@gyzerok Sure. It is an action creator.

// actions/location.js
export function routeLocationDidUpdate(location) {
  return {
    type: 'LOCATION_UPDATE',
    payload: {
      ...location,
    },
  };
}

@pburtchaell so in your example I dont quiet understand where do you fetch data nessesary for a particular route

Here's a fuller version of my example above:

https://github.com/acdlite/redux-react-router/blob/master/README.md#bonus-reacting-to-state-changes-with-redux-rx

I assume @pburtchaell would use an async version of that code to get data (from a REST call etc). What i'm unsure about is what then? I assume it then goes to a store, but would that store be a locationStore that i then

@connect

to in, let's say in

<Comments />

which would already be 'connected' (subscribed) to a commentStore? Or just add register these location actions in commentsStore?

An url like

/comments/123

is always going to be 'coupled' to the comments component, so how do i reuse it? Sorry if this is dumb, very confused here. Which there was a solid example I could find.

I'm also grappling with this, figuring out how to call my static fetchData(dispatch, params) method (static, because the server calls it to dispatch async FETCH actions before the initial render).

Since a reference to dispatch exists within the context, I'm thinking the cleanest way to get this for client-side calling of fetchData is a Connector-like component which calls fetchData or a shouldFetchData, or have the select callback get a reference to dispatch along with state, so we can inspect the current state of store and dispatch FETCH actions as required.

The latter is more concise but breaks the fact that select should remain a pure function. I'll look into the former.

My solution is this, although quite tailored to my set up (static fetchData(dispatch, state) method which dispatches async _FETCH actions) it'll call any callbacks passed to it with the appropriate properties: https://gist.github.com/grrowl/6cca2162e468891d8128 — doesn't use willTransitionTo, though, so you won't be able to delay the transitioning of pages.

We definitely need to add in the docs! Possible in "Using with react-router" section.

We'll have an official way of doing that after 1.0 release.

Great to hear @gaearon.

Then it's just a matter of using observable operators to subscribe to specific state changes.

I've got an observable stream set up to keep my store in sync with any route changes, and I've got a stream setup to watch my store too. I'm interested in transitioning to a new route when when a value changes in my store, specifically when it changes from undefined to a valid string as a result of successfully submitting a form elsewhere in my app.

The two streams are set up and working, and the store is being updated, but I'm new to Rx though, and having trouble with the observable query ... any pointers? Am I on the right track? Pretty sure its got something to do with distinctUntilChanged but its baking my noodle at the moment, any help appreciated :)

EDIT: Ack! Sorry, answered my own question. Pseudo code below. Let me know if it looks horrid?

const didChangeProject$ = state$
  .distinctUntilChanged(state => state.project._id)
  .filter(state => typeof state.project._id !== 'undefined');

Closing in favor of #177. When we get to documenting routing, we'll need to cover all scenarios: redirect on state change, redirect on request callback, etc.

I know this is closed.. but with React 0.14 and the various 1.0 dependencies in beta right now, is there an update to this and if not, can a tutorial around the new abilities in React 0.14, react-router, redux, etc be written? There seem to be a lot of us that are trying to stay up to date with the upcoming changes while learning/understanding all this at the same time. I know things are in a state of flux (pun.. uh.. not intended), but it seems I am seeing pieces from different examples that dont play nice with one another and after several days I still cant get my updated app to work with routing. Most likely my own fault as I am still struggling a bit with understanding how all this works, just hoping an updated tutorial with the latest stuff is out soon.

Until there is a tutorial feel free to look at examples/real-world which has the routing. It's not "latest and greatest" right now but it works fine.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

timdorr picture timdorr  ·  3Comments

ilearnio picture ilearnio  ·  3Comments

captbaritone picture captbaritone  ·  3Comments

amorphius picture amorphius  ·  3Comments

ms88privat picture ms88privat  ·  3Comments