-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
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
React router integration #177
Comments
This is essentially a side effect. Is there any reason this could not be implemented by subscribing to the store's change event? |
Wouldn't that be a side effect either? How would I get the transitionTo key from the action? |
Make a route reducer that saves the current route state. Then, transition from inside a store subscriber whenever the route state changes. |
makes sense :) thx! |
I think this is contrary how React Router works. When using RR with Flux, I think it's right to listen to the router's change event and fire app's Actions, which will in turn update the store with the new current path. The root component listens to that store and re-renders. So the normal flow is I think that any router transitions should be in action creators—because, effectively, export function loginSuccess(user, router) {
return dispatch => {
dispatch({ user, type: LOGIN_SUCCESS });
router.transitionTo('/dashboard'); // will fire CHANGE_ROUTE in its change handler
};
} It's up to the calling component to pass the router. |
Yeah, that's essentially how I've always done it, but I've never really liked how such a crucial piece of state was out of your control. In an ideal world, we'd keep route state inside our store, right? It was my understanding that with React Router 1.0, the browser history listening part, the route matching part, and the rendering part are all separated. So it seems like it should be possible to fire an action inside the history listener, and match + render inside the the store listener. // Fire action creator
// Store keeps track of current location
BrowserHistory.listen(location => updateRouteLocation(location));
store.subscribe(state => {
// Obviously you would want to memoize this
Router.match(state.location, (error, props) => {
// blah blah React.render() goes here
}
}) Huge disclaimer: I haven't actually implemented this yet, nor have I played with the RR beta, but it seems like it should be possible. |
This is precisely what I'm advocating :-) My point was that you don't want the custom action (e.g. |
What about the router reducer to handle the LOGIN_SUCCESS action like: export default function router(state = {}, action) {
switch (action.type) {
case LOGIN_SUCCESS:
return {
location: { pathname: '/' }
};
case LOGOUT:
return {
location: { pathname: '/login' }
};
default:
return state;
}
} Shouldn't the stores/reducers be responsible for changing the state? |
@gaearon Re-reading your comment I see we're mostly on the same page. My original comment should have said:
rather than transition. I disagree that the action creator is the right place to put EDIT: To elaborate store.subscribe(state => {
if (state.loggedIn && state.location == something) {
router.transitionTo(successLocation);
}
}); or something like that, you get the idea. |
@malte-wessel A core part of the Redux architecture is that we assume reducers are pure, with no side-effects. It's what enables stuff cool stuff time travel. |
I have now something like this function handleHistoryChange() {
redux.dispatch(historyChange({ history.location }));
}
history.addChangeListener(handleHistoryChange);
function handleRouteStateChange() {
const location = redux.getState().router.location;
// distinguish between changes that come from history
// (like pressing the back button) and those from actions...
// If the change comes from history return here (history is already in this state)
const fromHistory = redux.getState().router.fromHistory
if(fromHistory) return;
const { pathname, query, state } = location;
// You need to implement your own transitionTo method
history.pushState(state, makePath(pathname, query));
}
redux.subscribe(handleRouteStateChange); The history is shared with the router, so you don't have to implement your own router rendering export default function router(state = {}, action) {
switch (action.type) {
case HISTORY_CHANGE:
return {
location: action.location,
fromHistory: true
};
case LOGIN_SUCCESS:
return {
location: { pathname: '/' }
};
case LOGOUT:
return {
location: { pathname: '/login' }
};
default:
return state;
}
} @acdlite Isn't the route reducer pure? I think you could actually do time travel with this. |
I still think this goes against how RR works. RR can cancel the transition, for example. (Route In your example, the app would first think it's on the success page (and re-render), but then the router's transition method would be called, and router might redirect the user from a route that's not ready yet. For routing state, I think the router change event payload should be the source of truth. Otherwise the router is not in control. Our wish to transition is our wish, but ultimately the router should decide when and whether it happens. |
Here's another example. RR 1.0 supports async route configuration. This means that you might want to transition to If the router is in control, you tell it to |
@gaearon How is RR not in control? All I did was change the location of |
With this approach: export default function router(state = {}, action) {
switch (action.type) {
case HISTORY_CHANGE:
return {
location: action.location
};
case LOGIN_SUCCESS: // <------------ I don't think it's a good idea!
return {
location: { pathname: '/' }
}; Location is updated in response to user action ( All I'm saying is, the only thing that can change With your code sample: store.subscribe(state => {
if (state.loggedIn && state.location == something) {
router.transitionTo(successLocation);
}
}); I assume that |
I also don't think reactive approach makes sense here. This: store.subscribe(state => {
if (state.loggedIn && state.location == something) {
router.transitionTo(successLocation);
}
}); will also trigger if I press “Back”. The My point is, it's clearly a side effect, and it's a side effect relevant to a particular action creator. It's not a global side effect that happens every time you're on a specific page and a specific field is true. Therefore I think it belongs in the action creator. |
In RR 1.0 you don't have access to the router instance until it's rendered anywhere. This makes things even more complicated. I guess that if you do a history.pushState() the router will try to make the transition. If the transition is aborted the router will update the history again (I guess) and the router store/reducer will be in the correct state. |
From the components, you always have access to the router instance. (It's passed by context.) |
yes, but not from the action creators. except when you store a reference to the router when it's rendered. but I think thats not the way to go. |
You can pass the router instance to the action creators. You can also transition from the components themselves. |
Nah, that is only changed by
What does a "global" side effect mean in the context of Redux, where the entire state atom is emitted every single time? Yes, my example above is clearly naive. In reality you would only be responding to a subset of the state changes, either using Connector's It's just like using |
I just don't understand how you can tell between situation where you need to redirect (just authenticated) and where you don't need to redirect (authenticated long ago but state looks similar). It sounds much more complicated than just doing it imperatively wherever you're dispatching |
(In fact I tried both approaches when developing Stampsy. I had too much pain with “observable” approach when some new conditions caused some filter I wrote long ago to fire and cause unwanted redirects. In the end I went with the imperative approach and haven't had a problem ever since.) |
@gaearon Example: const stateStream = storeToStateStream(store);
stateStream
.distinctUntilChanged(state => !state.loggedIn && state.location === '/login')
.subscribe(state => {
if (state.loggedIn && state.location === '/login') {
// Only fires when you're on the login page and you just logged in
router.transitionTo('/success');
}
}); Edit: even better... const stateStream = storeToStateStream(store);
stateStream
.distinctUntilChanged(state => !state.loggedIn && state.location === '/login')
.filter(state => state.loggedIn && state.location === '/login')
.subscribe({
router.transitionTo('/success');
}); |
I like this reactive approach better because now it doesn't matter how the user logs in as long as the state field has changed. You can use a login form, a web socket event, whatever... no need for separate Clearly not all transitions should be done this way (not even most), but I think it's best for transitions that are side effects of Flux actions. |
Oh, I actually like it. I missed the distinct and “changed” bit. |
Also moving router.transitionTo from action creators to store subscriber makes code more independent of router realization, no need to pass router instance to action creators. (i can use middleware to remove this dep) But even for simple changeRoute action I need something like this const stateStream = storeToStateStream(store);
stateStream
.distinctUntilChanged(state => state.requestedRoutePath)
.filter(state => !!state.requestedRoutePath)
.filter(state => state.requestedRoutePath !== state.location)
.subscribe({
router.transitionTo(state.requestedRoutePath);
}); (It will not work if location changed elsewhere but requestedRoutePath still the same so need reset it in reducer on location change.) Moving 'router.transitionTo' to action, makes code simple and life more easier. |
@istarkov I wouldn't use a generic My point about doing transitions inside state subscribers regards only those route transitions that are a side effect of an action — like when a user logs in, or when a form is successfully submitted, etc. Good catch on using |
Is there a library or code sample used for the |
@timothyjlaurent the function storeToStateStream(store) {
return Rx.Observable.fromEventPattern(
(handler) => store.subscribe(handler),
(handler, unsubscribe) => unsubscribe(),
() => store.getState()
);
} |
@tappleby thanks for kindly clarifying |
Still having trouble figuring out using redux with RR... Specifically, RR 1.0 allows you to pass in an function requireAuth(nextState, transition) {
// My user state is in redux — how do I get it out!
}
export default (
<Route path="/" component={App}>
<Route path="login" component={LoginRoute}/>
<Route path="account" component={AccountRoute} onEnter={requireAuth}/>
<Route path="*" component={NotFound}/>
</Route>
); I've tried (and failed) at having a redux singleton contained within a module. The problem is I need the same instance of redux both inside my route definition (for the auth handler) and inside the client entry point (where I hydrate the redux instance and pass it into the Any ideas? Can I use context here somehow? |
@iest you can wrap your routes definition: // routes.js
export const createRoutes = (requireAccess) => {
return (
<Route name="app" component={App}>
<Route name="home" path="/" components={HomePage} onEnter={requireAccess(accessLevels.user)}/>
...
</Route>
);
}; And then bind requireAuth to redux and pass it to createRoutes. |
@gaearon Is there a gist somewhere with the current "best" practice (I know it's moving fast at the moment) to integrate RR1.0 with redux? |
Looks perfect @iNikNik! Thanks very much. |
While it's far from perfect API right now, something good (and official) is going to eventually come out of https://github.com/acdlite/redux-react-router so you might wanna watch it. Leaving this open so I'll come back to this in new docs. |
maybe react-router not suit for redux? |
+1 |
Superseded by #637 which contains more up-to-date info. |
@gaearon I am following approach for routing in action.. I am new to react and redux and got following issue:
Any suggestion ? |
Please ask this on StackOverflow as this is a usage question. We have |
I have asked it but no one answered :) ... http://stackoverflow.com/questions/35057443/redux-transition-to-after-action-execution |
I'm not sure if this was already considered. I'm currently playing around with a react-router integration and want my actions to return a transition statement:
In my case it's necessary to run the transition after the dispatch is finished. My idea was to compose the middleware stack like this:
The transition middleware should read the
transitionTo
key and invoke the routers transition method.Any idea how this could be done?
The text was updated successfully, but these errors were encountered: