You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/style-guide/style-guide.md
+75-1Lines changed: 75 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -286,10 +286,84 @@ It's a bit more typing, but it results in the most understandable code and state
286
286
287
287
### Treat Reducers as State Machines
288
288
289
-
Many Redux reducers are written "unconditionally". They only look at the dispatched action and calculate a new state value, without basing any of the logic on what the current state might be. This can cause bugs, as some actions may not be "valid" conceptually at certain times depending on the rest of the app logic. For example, a "request succeeded" action should only have a new value calculated if the state says that it's already "loading", or an "update this item" action could be dispatched even if there is no item marked as "being edited".
289
+
Many Redux reducers are written "unconditionally". They only look at the dispatched action and calculate a new state value, without basing any of the logic on what the current state might be. This can cause bugs, as some actions may not be "valid" conceptually at certain times depending on the rest of the app logic. For example, a "request succeeded" action should only have a new value calculated if the state says that it's already "loading", or an "update this item" action should only be dispatched if there is an item marked as "being edited".
290
290
291
291
To fix this, **treat reducers as "state machines", where the combination of both the current state _and_ the dispatched action determines whether a new state value is actually calculated**, not just the action itself unconditionally.
292
292
293
+
<details>
294
+
<summary>
295
+
<h4>Detailed Explanation</h4>
296
+
</summary>
297
+
298
+
A [finite state machine](https://en.wikipedia.org/wiki/Finite-state_machine) is a useful way of modeling something that should only be in one of a finite number of "finite states" at any time. For example, if you have a `fetchUserReducer`, the finite states can be:
299
+
300
+
-`"idle"` (fetching not started yet)
301
+
-`"loading"` (currently fetching the user)
302
+
-`"success"` (user fetched successfully)
303
+
-`"failure"` (user failed to fetch)
304
+
305
+
To make these finite states clear and [make impossible states impossible](https://kentcdodds.com/blog/make-impossible-states-impossible), you can specify a property that holds this finite state:
306
+
307
+
```js
308
+
constinitialUserState= {
309
+
status:'idle', // explicit finite state
310
+
user:null,
311
+
error:null
312
+
}
313
+
```
314
+
315
+
With TypeScript, this also makes it easy to use [discriminated unions](https://basarat.gitbook.io/typescript/type-system/discriminated-unions) to represent each finite state. For instance, if `state.status === 'success'`, then you would expect `state.user` to be defined and wouldn't expect `state.error` to be truthy. You can enforce this with types.
316
+
317
+
Typically, reducer logic is written by taking the action into account first. When modeling logic with state machines, it's important to take the state into account first. Creating "finite state reducers" for each state helps encapsulate behavior per state:
318
+
319
+
```js
320
+
import {
321
+
FETCH_USER,
322
+
// ...
323
+
} from'./actions'
324
+
325
+
constIDLE_STATUS='idle';
326
+
constLOADING_STATUS='loading';
327
+
constSUCCESS_STATUS='success';
328
+
constFAILURE_STATUS='failure';
329
+
330
+
constfetchIdleUserReducer= (state, action) => {
331
+
// state.status is "idle"
332
+
switch (action.type) {
333
+
caseFETCH_USER:
334
+
return {
335
+
...state,
336
+
status:LOADING_STATUS
337
+
}
338
+
}
339
+
default:
340
+
return state;
341
+
}
342
+
}
343
+
344
+
// ... other reducers
345
+
346
+
constfetchUserReducer= (state, action) => {
347
+
switch (state.status) {
348
+
caseIDLE_STATUS:
349
+
returnfetchIdleUserReducer(state, action);
350
+
caseLOADING_STATUS:
351
+
returnfetchLoadingUserReducer(state, action);
352
+
caseSUCCESS_STATUS:
353
+
returnfetchSuccessUserReducer(state, action);
354
+
caseFAILURE_STATUS:
355
+
returnfetchFailureUserReducer(state, action);
356
+
default:
357
+
// this should never be reached
358
+
return state;
359
+
}
360
+
}
361
+
```
362
+
363
+
Now, since you're defining behavior per state instead of per action, you also prevent impossible transitions. For instance, a `FETCH_USER` action should have no effect when `status === LOADING_STATUS`, and you can enforce that, instead of accidentally introducing edge-cases.
364
+
365
+
</details>
366
+
293
367
### Normalize Complex Nested/Relational State
294
368
295
369
Many applications need to cache complex data in the store. That data is often received in a nested form from an API, or has relations between different entities in the data (such as a blog that contains Users, Posts, and Comments).
0 commit comments