{"action":"created","issue":{"url":"https://api.github.com/repos/gaearon/redux/issues/437","labels_url":"https://api.github.com/repos/gaearon/redux/issues/437/labels{/name}","comments_url":"https://api.github.com/repos/gaearon/redux/issues/437/comments","events_url":"https://api.github.com/repos/gaearon/redux/issues/437/events","html_url":"https://github.com/gaearon/redux/issues/437","id":99905895,"number":437,"title":"Relax the constraint on actions","user":{"login":"gcanti","id":878912,"avatar_url":"https://avatars.githubusercontent.com/u/878912?v=3","gravatar_id":"","url":"https://api.github.com/users/gcanti","html_url":"https://github.com/gcanti","followers_url":"https://api.github.com/users/gcanti/followers","following_url":"https://api.github.com/users/gcanti/following{/other_user}","gists_url":"https://api.github.com/users/gcanti/gists{/gist_id}","starred_url":"https://api.github.com/users/gcanti/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gcanti/subscriptions","organizations_url":"https://api.github.com/users/gcanti/orgs","repos_url":"https://api.github.com/users/gcanti/repos","events_url":"https://api.github.com/users/gcanti/events{/privacy}","received_events_url":"https://api.github.com/users/gcanti/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/gaearon/redux/labels/discussion","name":"discussion","color":"fbca04"},{"url":"https://api.github.com/repos/gaearon/redux/labels/wontfix","name":"wontfix","color":"ffffff"}],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":6,"created_at":"2015-08-09T15:35:20Z","updated_at":"2015-08-09T18:02:45Z","closed_at":null,"body":"First of all thanks to all the contributors, in particular to @gaearon and @acdlite for your work.\r\n\r\n### The problem\r\n\r\nCurrently actions must be plain objects due to this invariant in the `dispatch` function:\r\n\r\n```js\r\n// createStore.js\r\n\r\ninvariant(\r\n  isPlainObject(action),\r\n  'Actions must be plain objects. Use custom middleware for async actions.'\r\n);\r\n```\r\n\r\nThis prevents to use constructors to build instances of actions. \r\nScanning the issues I found this comment by @gaearon in https://github.com/gaearon/redux/issues/355#issuecomment-125965512\r\n\r\n> 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\r\n\r\nBut I can't figure out why.\r\n\r\n### Relaxing the constraint seems to bring some benefits:\r\n\r\n- define actions as models (domain driven design)\r\n- possibly simplify the reducer and get rid of constants\r\n- type check the actions (for example defining actions with [tcomb](https://github.com/gcanti/tcomb), TypeScript or Flow)\r\n\r\n### Example (from the counter app example)\r\n\r\n**Define actions as models**\r\n\r\n```js\r\n// CounterAction.js\r\n\r\nclass IncrementEvent {\r\n  patch(state) {\r\n    return state + 1;\r\n  }\r\n}\r\n\r\nclass DecrementEvent {\r\n  patch(state) {\r\n    return state - 1;\r\n  }\r\n}\r\n\r\nexport function increment() {\r\n  return new IncrementEvent();\r\n}\r\n\r\nexport function decrement() {\r\n  return new DecrementEvent();\r\n}\r\n```\r\n\r\n**Simplify the reducer and get rid of constants**\r\n\r\n```js\r\n// counter.js\r\n\r\nexport default function counter(state = 0, action) {\r\n  if (typeof action.patch === 'function') { \r\n    return action.patch(state); // get rid of switch and constants\r\n  }\r\n  return state;\r\n}\r\n```\r\n\r\nThe implementation above promotes a **patch function** (instead of a reduce function) as a possible primitive:\r\n\r\n```js\r\npatch :: (state) -> state\r\n```\r\n\r\n**Type check the action construction** (here with the help of tcomb)\r\n\r\n```js\r\n// CounterAction.js\r\n\r\nimport t from 'tcomb';\r\n\r\n//\r\n// use the constructors as (type checked) action creators \r\n// Instances are also frozen\r\n//\r\n\r\nexport var IncrementEvent = t.struct({});\r\nIncrementEvent.prototype.patch = function (state) {\r\n  return state + 1;\r\n};\r\n\r\nexport var DecrementEvent = t.struct({});\r\nDecrementEvent.prototype.patch = function (state) {\r\n  return state - 1;\r\n};\r\n```\r\n"},"comment":{"url":"https://api.github.com/repos/gaearon/redux/issues/comments/129222388","html_url":"https://github.com/gaearon/redux/issues/437#issuecomment-129222388","issue_url":"https://api.github.com/repos/gaearon/redux/issues/437","id":129222388,"user":{"login":"gaearon","id":810438,"avatar_url":"https://avatars.githubusercontent.com/u/810438?v=3","gravatar_id":"","url":"https://api.github.com/users/gaearon","html_url":"https://github.com/gaearon","followers_url":"https://api.github.com/users/gaearon/followers","following_url":"https://api.github.com/users/gaearon/following{/other_user}","gists_url":"https://api.github.com/users/gaearon/gists{/gist_id}","starred_url":"https://api.github.com/users/gaearon/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gaearon/subscriptions","organizations_url":"https://api.github.com/users/gaearon/orgs","repos_url":"https://api.github.com/users/gaearon/repos","events_url":"https://api.github.com/users/gaearon/events{/privacy}","received_events_url":"https://api.github.com/users/gaearon/received_events","type":"User","site_admin":false},"created_at":"2015-08-09T18:02:45Z","updated_at":"2015-08-09T18:02:45Z","body":"Let's keep it open for discussion. I'll come back eventually and write a more thorough response. Busy with getting 1.0 out :-)"}}