Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

firing actions in response to route transitions in react-router #227

Closed
deowk opened this issue Jul 7, 2015 · 26 comments
Closed

firing actions in response to route transitions in react-router #227

deowk opened this issue Jul 7, 2015 · 26 comments

Comments

@deowk
Copy link

deowk commented Jul 7, 2015

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.

@johanneslumpe
Copy link
Contributor

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

@deowk
Copy link
Author

deowk commented Jul 7, 2015

@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?

@johanneslumpe
Copy link
Contributor

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

@acdlite
Copy link
Collaborator

acdlite commented Jul 7, 2015

@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!

@gaearon
Copy link
Contributor

gaearon commented Jul 7, 2015

We need this in new docs. So well written.

@deowk
Copy link
Author

deowk commented Jul 7, 2015

@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?
};

@tshddx
Copy link

tshddx commented Jul 8, 2015

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

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

@danielberndt
Copy link

@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);
});

@emmenko
Copy link
Contributor

emmenko commented Jul 8, 2015

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

@pburtchaell
Copy link

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)));

@abrkn
Copy link

abrkn commented Jul 11, 2015

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

@pburtchaell
Copy link

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.

@gyzerok
Copy link

gyzerok commented Jul 11, 2015

@pburtchaell can you share your routeLocationDidUpdate?

@pburtchaell
Copy link

@gyzerok Sure. It is an action creator.

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

@gyzerok
Copy link

gyzerok commented Jul 12, 2015

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

@acdlite
Copy link
Collaborator

acdlite commented Jul 12, 2015

@swordsreversed
Copy link

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.

@grrowl
Copy link

grrowl commented Jul 13, 2015

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.

@grrowl
Copy link

grrowl commented Jul 13, 2015

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.

@gyzerok
Copy link

gyzerok commented Jul 13, 2015

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

@gaearon
Copy link
Contributor

gaearon commented Jul 13, 2015

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

@swordsreversed
Copy link

Great to hear @gaearon.

@jedrichards
Copy link

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');

@gaearon
Copy link
Contributor

gaearon commented Aug 14, 2015

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.

@gaearon gaearon closed this as completed Aug 14, 2015
@justjacksonn
Copy link

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.

@gaearon
Copy link
Contributor

gaearon commented Sep 23, 2015

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests