Closed
Description
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:
// createStore.js
invariant(
isPlainObject(action),
'Actions must be plain objects. Use custom middleware for async actions.'
);
This prevents to use constructors to build instances of actions.
Scanning the issues I found this comment by @gaearon in #355 (comment)
record/replay and associated features won't work with an action class. We don't want to encourage this so people don't lock themselves out of great features
But I can't figure out why.
Relaxing the constraint seems to bring some benefits:
- define actions as models (domain driven design)
- possibly simplify the reducer and get rid of constants
- type check the actions (for example defining actions with tcomb, TypeScript or Flow)
Example (from the counter app example)
Define actions as models
// CounterAction.js
class IncrementEvent {
patch(state) {
return state + 1;
}
}
class DecrementEvent {
patch(state) {
return state - 1;
}
}
export function increment() {
return new IncrementEvent();
}
export function decrement() {
return new DecrementEvent();
}
Simplify the reducer and get rid of constants
// counter.js
export default function counter(state = 0, action) {
if (typeof action.patch === 'function') {
return action.patch(state); // get rid of switch and constants
}
return state;
}
The implementation above promotes a patch function (instead of a reduce function) as a possible primitive:
patch :: (state) -> state
Type check the action construction (here with the help of tcomb)
// CounterAction.js
import t from 'tcomb';
//
// use the constructors as (type checked) action creators
// Instances are also frozen
//
export var IncrementEvent = t.struct({});
IncrementEvent.prototype.patch = function (state) {
return state + 1;
};
export var DecrementEvent = t.struct({});
DecrementEvent.prototype.patch = function (state) {
return state - 1;
};