|
2 | 2 |
|
3 | 3 |  |
4 | 4 |
|
5 | | -*A tiny library for creating state machines in Redux apps.* |
6 | | - |
| 5 | +*A tiny library (12 lines) for creating state machines as swappable Redux reducers* |
7 | 6 |
|
8 | 7 | redux-machine enables you to create[reducers](http://redux.js.org/docs/basics/Reducers.html) that can transition between different "statuses." These are likes states in a[finite state machine](https://en.wikipedia.org/wiki/Finite-state_machine). The goal is for redux-machine to support complex workflows simply while keeping all state in the redux store. Keeping all state in the store is good because: |
9 | 8 |
|
10 | 9 | - redux-machine works with time-travel debugging. Time-travel debugging was the main[motivation for building redux itself](https://www.youtube.com/watch?v=xsSnOQynTHs). |
11 | 10 | - Debugging is easy because information is in one place (the store). |
12 | 11 | - Statuses such are queryable by the user interface. This is helpful if you want to show things to the user such as loading spinners to indicate status |
13 | 12 |
|
14 | | -redux-saga and redux-observable also make it easy to create workflows in redux apps. They are more beautiful and powerful than redux-machine, but can break the "all state is in the store" paradigm. |
15 | | - |
16 | 13 | ##Install |
17 | 14 |
|
18 | 15 | `npm install redux-machine --save` |
19 | 16 |
|
20 | | ->redux-machine internally uses Object.assign and Symbol, whichareES2015features. If you need to support older browsers, you can use a polyfill such as[core-js](https://github.com/zloirock/core-js#basic). |
| 17 | +>redux-machine internally uses Object.assign, whichis anES2015feature. If you need to support older browsers, you can use a polyfill such as[core-js](https://github.com/zloirock/core-js#basic). |
21 | 18 |
|
22 | | -##API |
| 19 | +##How to Use |
23 | 20 |
|
24 | | --**`createMachine(reducerObject)`** returns a machine, which is itself a reducer |
25 | | - |
26 | | -`reducerObject` is an object where the keys are statuses and the values are sub-reducers. For example, this code creates a reducer that acts like`initReducer` when the status is`INIT` and acts like`inProgressReducer` when the status is`IN_PROGRESS`: |
| 21 | +This is the entire API for redux-machine: |
27 | 22 |
|
28 | 23 | ```js |
| 24 | +// entire API, no middleware required |
| 25 | +import {createMachine } =from'./index.js' |
| 26 | + |
29 | 27 | constfetchUsersReducer=createMachine({ |
30 | 28 | 'INIT': initReducer, |
31 | 29 | 'IN_PROGRESS': inProgressReducer |
32 | 30 | }) |
33 | 31 | ``` |
34 | 32 |
|
35 | | -`reducerObject` must have an`INIT` key. The value for the`INIT` key is the reducer for the starting status of the machine/reducer. |
36 | | - |
37 | | -The reducer returned by`createMachine` adds a`STATUS` key to the store. That is, when the status is`INIT`, the value of`STATUS` is`INIT` and when the status is`IN_PROGRESS`, the value of`STATUS` is`IN_PROGRESS`. This is useful for debugging or for acting on the current status in the UI or in your API callers. |
38 | | - |
39 | | --**`become`** (symbol) |
40 | | -The reducers that are the values of`reducersObject` can use`become` to transition the machine/reducer to a different status. For example, the following code in`initReducer` transitions`fetchUsersReducer` to the`IN_PROGRESS` status when the`` action is dispatched. At that point,`fetchUsersReducer` will act like`inProgressReducer` and the value of`STATUS` changes to`IN_PROGRESS`: |
41 | | - |
42 | | -```js |
43 | | -import {become }from'redux-machine' |
44 | | - |
45 | | -... |
46 | | -switch (action.type) { |
47 | | -case'FETCH_USERS': |
48 | | -returnObject.assign({}, state, { |
49 | | - error:null, |
50 | | - [become]:'IN_PROGRESS' |
51 | | - }) |
52 | | -case default: |
53 | | -return state |
54 | | - } |
55 | | -``` |
56 | | - |
57 | | ->redux-machine's`become` is influenced by[Akka's`become`](http://doc.akka.io/docs/akka/snapshot/scala/actors.html#become-unbecome). |
58 | | -
|
59 | | -##Full Example |
60 | | - |
61 | | -Suppose you are making an API call to fetch users, and want only one instance of this API call to happen at a time. You also want to communicate to the user the status of the API call:`INIT` (initial status, no call in progress) and`IN_PROGRESS`. |
62 | | - |
63 | | -You can use redux-machine to have the following status transitions in your reducer: |
64 | | - |
65 | | -- When the status is`INIT` and the action type is`FETCH_USERS`, the machine transitions to`IN_PROGRESS` status. |
66 | | -- When the status is`IN_PROGRESS` and the action type is`FETCH_USERS_RESPONSE` or`FETCH_USERS_FAIL`, the machine transitions to the`INIT` (initial) status. |
67 | | - |
68 | | -I'll walk through how to create the machine in this example. If you prefer to see all the code in one place, it's in`./test.js`. |
69 | | - |
70 | | -###`import` redux-machine |
71 | | - |
72 | | -```js |
73 | | -import {createMachine,become }from'./index.js' |
74 | | -``` |
75 | | - |
76 | | -###Create Reducers |
| 33 | +The reducer returned by`createMachine` will act like`initReducer` when its status is`INIT` and will act like`inProgressReducer` when the status is`IN_PROGRESS`. If the store's`state.status` is undefined, the reducer for`INIT` is used (so it's a good idea to provide a reducer for the`INIT` status). |
| 34 | +>>>>>>>update API |
77 | 35 |
|
78 | | -Next, define a reducer for eachstatusin the machine ('INIT' and 'IN_PROGRESS'): |
| 36 | +`initReducer` and`inProgressReducer` can dostatustransitions by setting`state.status`: |
79 | 37 |
|
80 | 38 | ```js |
81 | 39 | constinitReducer= (state= {error:null, users: []},action)=> { |
82 | | - |
83 | 40 | switch (action.type) { |
84 | 41 | case'FETCH_USERS': |
85 | 42 | returnObject.assign({}, state, { |
86 | 43 | error:null, |
87 | | - [become]:'IN_PROGRESS' |
| 44 | +// transition to a different status! |
| 45 | + status:'IN_PROGRESS' |
88 | 46 | }) |
89 | 47 | default: |
90 | 48 | return state |
91 | 49 | } |
92 | 50 | } |
93 | 51 |
|
94 | 52 | constinProgressReducer= (state= {},action)=> { |
95 | | - |
96 | 53 | switch (action.type) { |
97 | 54 | case'FETCH_USERS_RESPONSE': |
98 | 55 | returnObject.assign({}, state, { |
99 | 56 | error:null, |
100 | 57 | users:action.payload.users, |
101 | | - [become]:'INIT' |
| 58 | +// transition to a different status! |
| 59 | + status:'INIT' |
102 | 60 | }) |
103 | 61 | case'FETCH_USERS_FAIL': |
104 | 62 | returnObject.assign({}, state, { |
105 | | - error:'fail', |
106 | | - [become]:'INIT' |
| 63 | + error:action.payload.error, |
| 64 | +// transition to a different status! |
| 65 | + status:'INIT' |
107 | 66 | }) |
108 | 67 | default: |
109 | 68 | return state |
110 | 69 | } |
111 | 70 | } |
112 | 71 | ``` |
113 | 72 |
|
114 | | ->The only special part of these reducers is how they use the`become` symbol, which I'll explain in the next section. |
115 | | -
|
116 | | -###Create the Machine |
117 | | - |
118 | | -Here's how you can use the`createMachine` function to create a reducer from the`initReducer` and`inProgressReducer`: |
119 | | - |
120 | | -```js |
121 | | -constfetchUsersReducer=createMachine({ |
122 | | -'INIT': initReducer, |
123 | | -'IN_PROGRESS': inProgressReducer |
124 | | -}) |
125 | | -``` |
126 | | - |
127 | | ->`fetchUsersReducer` is a normal[reducer](http://redux.js.org/docs/basics/Reducers.html) that you can use directly in your Redux app, with no need for special middleware. |
| 73 | +The example above defines the following state machine: |
128 | 74 |
|
129 | | -`initReducer` is the value for`INIT`, so`createMachine` knows to start off acting like`initReducer`. |
130 | | - |
131 | | -In the object provided to`createMachine`, the names of the keys matter because they are used within reducers for transitioning the machine/reducer between statuses. For example, the following code in`initReducer` says that when the action is of type`FETCH_USERS`, transition the machine to the`IN_PROGRESS` status: |
132 | | - |
133 | | -```js |
134 | | -switch (action.type) { |
135 | | -case'FETCH_USERS': |
136 | | -returnObject.assign({}, state, { |
137 | | - error:null, |
138 | | - [become]:'IN_PROGRESS' |
139 | | - }) |
140 | | -case default: |
141 | | -return state |
142 | | - } |
143 | | -``` |
| 75 | + |
144 | 76 |
|
145 | | ->`become` is a symbol imported from redux-machine, not a string. See the`import` statement in the example above. |
| 77 | +In words: |
| 78 | +- When the status is`INIT` and the action type is`FETCH_USERS`, the machine transitions to`IN_PROGRESS` status. |
| 79 | +- When the status is`IN_PROGRESS` and the action type is`FETCH_USERS_RESPONSE` or`FETCH_USERS_FAIL`, the machine transitions to the`INIT` (initial) status. |
146 | 80 |
|
147 | 81 | ##Asynchronous Effects |
148 | 82 |
|
149 | 83 | redux-machine does only one thing: help you model explicit status transitions in reducers. It doesn't prescribe a way of handling asynchronous effects such as API calls. This leaves it open for you to use[no async effects library](http://stackoverflow.com/a/34599594/2482570),[redux-loop](https://github.com/redux-loop/redux-loop),[redux-thunk](https://github.com/gaearon/redux-thunk),[redux-saga](https://github.com/yelouafi/redux-saga), or anything else. |
150 | 84 |
|
151 | | -That said, redux-machine fits very naturally withredux-loop, since both enhancehow you can usereducers. Here's how you could use redux-machine with redux-loop: |
| 85 | +That said, redux-machine fits very naturally withother tools which enhancethe expressiveness ofreducers, such as redux-loop and redux-side-effect. Here's how you could use redux-machine with redux-loop: |
152 | 86 |
|
153 | 87 | ```js |
154 | 88 | import {loop,Effects }from'redux-loop' |
|