-
-
Notifications
You must be signed in to change notification settings - Fork 15.2k
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
Relax the constraint on actions #437
Comments
👍 This would be a great addition. |
This is very much opposite to how Redux works. How would you serialize them and then replay these actions from a deserialized object? |
This page should cover why we don't ever want to do this: http://gaearon.github.io/redux/docs/recipes/ReducingBoilerplate.html It also explains how to avoid
|
There is work on getting actions to type check. See #290, #356. It doesn't mean they have to be something other than plain objects. Discriminated unions FTW! You can also easily write middleware to do similar runtime action checking against a schema. |
Hi @gaearon ! thanks for your comments.
I'm not proposing a change to how Redux works, the reducer function (and its signature) is the "heart" of Redux, I'm absolutely fine with that. I'm just proposing to relax a constraint on actions in order to allow a different implementation (using dynamic dispatch on a polymorphic operation instead of a switch) but there will still be concrete and serializable actions. Take this action (from the todomvc example): class AddTodoEvent {
constructor(text) {
this.text = text;
}
patch(state) {
return [{
id: (state.length === 0) ? 0 : state[0].id + 1,
marked: false,
text: this.text
}, ...state]
}
} The additional benefit here is that the action payload and the effect on the state are close, in the same file, in the same definition. The For example, in order to be "serialization-compliant" a user must provide:
It's a problem a user must solve in any case in order to use, for example, a export function increment() {
return {
type: INCREMENT_COUNTER,
dateOfIncrement: new Date()
};
} otherwise you'll end up with a very constrained action system (basically just JSONs). Flow is interesting indeed! I spent several weeks on it, but at the moment is incompatible with all the features provided by babel (no |
Let's keep it open for discussion. I'll come back eventually and write a more thorough response. Busy with getting 1.0 out :-) |
That's the point of Redux. You get awesome features (hot reloading, time travel, etc) without asking user to provide stuff like |
There's nothing inherently Flow-ish there. All I'm saying is that checking plain objects is just as expressive as attaching runtime check semantics to classes. What's the difference? It's like |
I don't see this as a benefit at all. In large codebases, you'll want to update absolutely different parts of state that are implementing different features, in response to the same action. Flux seems “complicated” because it forces your actions to be descriptions, not implementations, but that's what it makes it powerful. Two people can work on different parts of app and react to the same action without ever having to see each other's model update logic. This is the decoupling that Redux provides (just like Flux). “Update functions” that operate on the whole state are cool but it's a different architecture than Redux that (in my experience) scales worse for bigger projects. And, as I described above, it loses some of its benefits. Why not split your reducer instead of making actions “smart”? In Redux, stuff like Don't underestimate the power of functions! |
I see, it seems sensible to me, but then there's still a problem with the invariant: this time it might be too loose. Let's define the property of being serializable in a "just work" way: Definition. An object
holds. Note. Being just-serializable is a special case of being "serialization-compliant" where There are 2 cases:
I'd prefer 2, but let's follow the 1 route. The invariant should then become something like: invariant(
isJustSerializable(action), // isPlainObject is too weak
'Actions must be "just serializable"'
); and actions created with: export function increment() {
return {
type: INCREMENT_COUNTER,
dateOfIncrement: new Date() // a Date instance is not just-serializable
};
}
// or...
// snippet from "Tutorial: Handcrafting an Isomorphic Redux Application (With Love)"
// https://medium.com/@bananaoomarang/handcrafting-an-isomorphic-redux-application-with-love-40ada4468af4
export function createTodo(text) {
return {
type: 'CREATE_TODO',
promise: request.post(BACKEND_URL, { text }) // a promise is not just-serializable
};
} will be invalid. TL;DR the current invariant should be relaxed or tightened? |
Checking if an action is serializable on each dispatch is probably not feasible due to performance concerns. Checking a plain object seems like a worth it trade off. It's cheap and covers most of the cases. You can currently get what you want by attaching a symbol to a POJO and then that'll eliminate switch statements. |
Yes, I would prefer to see it tightened in development. We might do this in later versions or as part of optional developer tools. |
Actually this is wrong: // or...
// snippet from "Tutorial: Handcrafting an Isomorphic Redux Application (With Love)"
// https://medium.com/@bananaoomarang/handcrafting-an-isomorphic-redux-application-with-love-40ada4468af4
export function createTodo(text) {
return {
type: 'CREATE_TODO',
promise: request.post(BACKEND_URL, { text }) // a promise is not just-serializable
};
} It is meant to be processed by middleware. By the time Promise middleware processes the action, whatever it dispatches, will be serializable. See Async Actions guide. For now, I'm closing because I can't see anything actionable here. If we can check actions before they're dispatched, I'd like to see that prototyped in the userland first anyway. |
coming back on this from a slightly different perspective, chrome/v8 tends to optimize classes much better then it optimizes pojos so something like
is going to be optimizable by v8 much better then export function increment() {
return {
type: INCREMENT_COUNTER,
payload: new Date()
};
} |
Dispatching thousands of actions per second (which is the only use case where this would matter) is not something you'd do in real Redux apps. They are meant to be perceived by a user, after all. And it's impossible to update the UI that fast anyway. |
it could also help the central dispatch function get optimized as currently I don't think any of the actions will share the same hidden type so it would be compiled. Just a gocha I ran into when my node.js instincts of 'making the same object literal a bunch, probably want to turn it into a simple class' kicked in. |
I'm sorry for bumping old issue, but will be really great to have the ability to check object types in middlewares to intercept the actions of specific types. For example, we could simple check |
@iotch relevant discussion here gcanti/redux-tcomb#9 if you want to chime in |
First of all thanks to all the contributors, in particular to @gaearon and @acdlite for your work.
The problem
Currently actions must be plain objects due to this invariant in the
dispatch
function:This prevents to use constructors to build instances of actions.
Scanning the issues I found this comment by @gaearon in #355 (comment)
But I can't figure out why.
Relaxing the constraint seems to bring some benefits:
Example (from the counter app example)
Define actions as models
Simplify the reducer and get rid of constants
The implementation above promotes a patch function (instead of a reduce function) as a possible primitive:
Type check the action construction (here with the help of tcomb)
The text was updated successfully, but these errors were encountered: