-
-
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
Forbid dispatch from inside a reducer #368
Comments
Another note: need another test to check that an error inside a reducer resets the flag back, i.e. there should be |
I'm on it @gaearon |
@gaearon what is the reason for adding this? I've disliked it in every Flux implementation that has it, and not just because the lifecycle issues. |
@phated Simply because this is never a valid use case. Reducers must be pure. |
In Flux there are valid-ish situations when you'd rather wish this check didn't exist, but in Redux there's just no excuse for dispatching inside reducer. It's never the right solution, and it only indicates a problem in the code. |
Can't it be implemented as middleware and be opt-in or opt-out. Having explicit throws that you can't turn off in library code is a pain. |
For complicated edge cases there are middlewares. |
Maybe it should just be a warning then? |
If it's a middleware, I doubt anybody who'd be affected by the error (beginners) would know to use it. |
In fact a custom middleware can opt you “out” of this behavior by queueing the “bad” dispatch on a next tick. But I still can't envision a scenario where you'd need it. |
I've been thinking about API design a lot lately and I've come to determine that authors can't see all use cases that consumers will do, so I think avoiding things that completely break them (like throwing) should be avoided. I agree that it is incorrect usage, so I think there should be a friendly warning (like React's dev warnings) but not an explicit throw. |
Generally I agree with your statement. It's a tricky line. For example, Redux doesn't try to enforce that you don't mutate the state. Mutating is the wrong thing to do and is a common source of mistakes, but we recognize that people sometimes might have valid reasons to mutate. We might warn them in the future, we might offer opt-in freeze-all-state plugins, but we won't disallow it. However there are some fundamental assumptions a library can make too. In case of Redux, it's a fundamental assumption that reducers don't cause side effects. You can always trivially have an upgrade path by moving this As you said, React has dev warnings, it's true. But if you try to Dispatching from inside a reducer is never correct because you'll overwrite the result of your first dispatch. It will be lost and unaccounted for, and will seem like a weird bug to the consumer. This is the reason it's better to throw and educate and offer an upgrade path, than to allow the wrong behavior. |
In other words, throwing will break them explicitly, but allowing to do that will break them implicitly. |
looks good @quicksnap @gaearon going to move to the next things |
@knowbody Sorry about the collision of work! I was almost done with it and then needed to run to a meeting. |
@quicksnap it's totally cool. I don't mind. and we are another step closer to the 1.0 release 😄 |
Fixed by #372. |
@gaearon When starting with redux I was angry I couldn't dispatch from a reducer in certain cases. I agree it shouldn't be done to keep reducers pure but I never really found a good workaround, all my workarounds suck. For example, if a REST call fails, I'd want to display a basic error modal. export function fooBarReducer(state = {}, action) {
switch (action.type) {
case types.GET_SPECIFIC_DATA_ERROR:
dispatch(openErrorModal(errorTypes.FAILED_REST_CALL));
return {
...state,
loading: false,
};
default:
return state;
}
} It seems silly but the only alternative I've come up with is this: // reducer
export function fooBarReducer(state = {}, action) {
switch (action.type) {
case types.GET_SPECIFIC_DATA_ERROR:
return {
...state,
loading: false,
error: true,
};
default:
return state;
}
}
// component
export class FooBarLayout extends React.Component {
componentDidUpdate(nextProps) {
const { error, dispatch } = this.props;
if (!error && nextProps.error) {
dispatch(openErrorModal(errorTypes.FAILED_REST_CALL));
}
}
render() {
// note: <ErrorModal /> is near the root level.
return (
<div>
<FooBarComponent />
<LoadingOverlay loading={this.props.loading} />
</div>
);
}
} This works just fine but I can't use a function for the component anymore because I need the react lifecycle events. There are different variations of this code where the error modal is in this component but it require the component to be rendered back to back twice. Do you have a good solution for this problem? It seems like most async events have this problem and it's hard to find a nice clean solution. Other examples would be:
|
This is one alternative. Another alternative is to use Redux Thunk and add this logic to your thunk action creator. The "async" example in the repo shows this. Finally you can check out redux-saga project which addresses this specific problem. |
@bpmckee, try thinking of your modal not like you need to "openErrorModal" (imperative state transition) but like "isErrorModalOpen" currently and which "error" should it display (declarative state), and then connect your React component that displays the modal to that state--you'll get this state into the component props and will render the modal like any other component. You need to store the error type in the state, too, not just "error: true", and reset the error with an action when it's time to dismiss the modal. |
@gaearon redux-saga looks very interesting I'll have to experiment with it. My first impressions are that it looks very promising though. @sompylasar I was thinking about the declarative state for the modal but the more I thought about it, the more complex I'd have to make the state object for that modal. In hindsight I probably should have went with it but there were too many issues going through my mind earlier. Such as what happens if the user hits back, what if I want a different part of the view to still reflect an error occurred even after the error modal was closed, etc. Anyway, thank you both for the helpful info! Sorry it ended up hijacking this thread. |
Also see https://github.com/redux-loop/redux-loop |
After reading reduxjs/redux#368, I've come to realize why reducers need to be pure and especially why they can't and shouldn't dispatch actions while the state is in limbo. Kinda like the same as react forbidding setState() in the render method. So, moved the trigger for the fetch posts action to the change category action creator.
We should throw if you attempt to dispatch while the reducer is being called.
Note that this will not cause the dreaded
Cannot dispatch in a middle of dispatch
if youdispatch
from component'scomponentDidMount
orcomponentWillReceiveProps
. Unlike Flux, we don't have this problem, because we call all subscribers after the reducer is called.This means we can set a flag immediately before
currentState = reducer(currentState, action)
increateStore
and reset it on the next line, so everything happens before the subscribers are called. The subscribers should be able to calldispatch
just fine.I'd very much like to see this as a PR as I don't have much time. It needs to:
Please build it on top of
breaking-changes-1.0
branch.The text was updated successfully, but these errors were encountered: