Skip to content

Commit b1bdb97

Browse files
deniswmarkerikson
authored andcommitted
Rewrite docs for createReducer() (#74)
* Rewrite docs for createReducer() This version tries to state more clearly the motivation behind the function and how the immer-powered state mutation works. * Add intro statement
1 parent b5f9b06 commit b1bdb97

File tree

1 file changed

+92
-44
lines changed

1 file changed

+92
-44
lines changed

docs/api/createReducer.md

Lines changed: 92 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,68 +5,116 @@ sidebar_label: createReducer
55
hide_title: true
66
---
77

8-
# `createReducer`
8+
# `createReducer()`
99

10-
A utility function to create reducers that handle specific action types, similar to the example function in the ["Reducing Boilerplate" Redux docs page](https://redux.js.org/recipes/reducing-boilerplate#generating-reducers). Takes an initial state value and an object that maps action types to case reducer functions. Internally, it uses the [`immer` library](https://github.com/mweststrate/immer), so you can write code in your case reducers that mutates the existing `state` value, and it will correctly generate immutably-updated state values instead.
10+
A utility that simplifies creating Redux reducer functions, by defining them as lookup tables of functions to handle each action type. It also allows you to drastically simplify immutable update logic, by writing "mutative" code inside your reducers.
1111

12-
```ts
13-
function createReducer(
14-
initialState: State,
15-
actionsMap: Object<String, Function>
16-
) {}
17-
```
18-
19-
Example usage:
12+
Redux [reducers](https://redux.js.org/basics/reducers) are often implemented using a `switch` statement, with one `case` for every handled action type.
2013

2114
```js
22-
import { createReducer } from 'redux-starter-kit'
15+
function counterReducer(state = 0, action) {
16+
switch (action.type) {
17+
case 'increment':
18+
return state + action.payload
19+
case 'decrement':
20+
return state - action.payload
21+
default:
22+
return state
23+
}
24+
}
25+
```
2326

24-
function addTodo(state, action) {
25-
const { newTodo } = action.payload
27+
This approach works well, but is a bit boilerplate-y and error-prone. For instance, it is easy to forget the `default` case or setting the initial state.
2628

27-
// Can safely call state.push() here
28-
state.push({ ...newTodo, completed: false })
29-
}
29+
The `createReducer` helper streamlines the implementation of such reducers. It takes two arguments. The first one is the initial state. The second is an object mapping from action types to _case reducers_, each of which handles one specific action type.
3030

31-
function toggleTodo(state, action) {
32-
const { index } = action.payload
31+
```js
32+
const counterReducer = createReducr(0, {
33+
increment: (state, action) => state + action.payload,
34+
decrement: (state, action) => state - action.payload
35+
})
36+
```
3337

34-
const todo = state[index]
35-
// Can directly modify the todo object
36-
todo.completed = !todo.completed
37-
}
38+
If you created action creators using `createAction()`, you can use those directly as keys for the case reducers.
39+
40+
```js
41+
const increment = createAction('increment')
42+
const decrement = createAction('decrement')
3843

39-
const todosReducer = createReducer([], {
40-
ADD_TODO: addTodo,
41-
TOGGLE_TODO: toggleTodo
44+
const counterReducer = createReducr(0, {
45+
[increment]: (state, action) => state + action.payload,
46+
[decrement]: (state, action) => state - action.payload
4247
})
4348
```
4449

45-
This doesn't mean that you _have to_ write code in your case reducers that mutates the existing `state` value, you can still write code that updates the state immutably. You can think of `immer` as a safety net, if the code is written in a way that mutates the state directly, `immer` will make sure that such update happens immutably. On the other hand the following code is still valid:
50+
## Direct State Mutation
51+
52+
Redux requires reducer functions to be pure and treat state values as immutable. While this is essential for making state updates predictable and observable, it can sometimes make the implementation of such updates awkward. Consider the following example:
4653

4754
```js
48-
import { createReducer } from 'redux-starter-kit'
55+
const addTodo = createAction('todos/add')
56+
const toggleTodo = createAction('todos/toggle')
57+
58+
const todosReducer = createReducr([], {
59+
[addTodo]: (state, action) => {
60+
const todo = action.payload
61+
return [...state, todo]
62+
},
63+
[toggleTodo]: (state, action) => {
64+
const index = action.payload
65+
const todo = state[index]
66+
return [
67+
...state.slice(0, index),
68+
{ ...todo, completed: !todo.completed }
69+
...state.slice(index + 1)
70+
]
71+
}
72+
})
73+
```
4974

50-
function addTodo(state, action) {
51-
const { newTodo } = action.payload
75+
The `addTodo` reducer is pretty easy to follow if you know the [ES6 spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax). However, the code for `toggleTodo` is much less straightforward, especially considering that it only sets a single flag.
5276

53-
// Updates the state immutably without relying on immer
54-
return [...state, { ...newTodo, completed: false }]
55-
}
77+
To make things easier, `createReducer` uses [immer](https://github.com/mweststrate/immer) to let you write reducers as if they were mutating the state directly. In reality, the reducer receives a proxy state that translates all mutations into equivalent copy operations.
5678

57-
function toggleTodo(state, action) {
58-
const { index } = action.payload
79+
```js
80+
const addTodo = createAction('todos/add')
81+
const toggleTodo = createAction('todos/toggle')
82+
83+
const todosReducer = createReducr([], {
84+
[addTodo]: (state, action) => {
85+
// This push() operation gets translated into the same
86+
// extended-array creation as in the previous example.
87+
state.push(todo)
88+
},
89+
[toggleTodo]: (state, action) => {
90+
// The "mutating" version of this case reducer is much
91+
// more direct than the explicitly pure one.
92+
const index = action.payload
93+
const todo = state[index]
94+
todo.completed = !todo.completed
95+
}
96+
})
97+
```
5998

60-
const todo = state[index]
61-
// Updates the todo object immutably without relying on immer
62-
return state.map((todo, i) => {
63-
if (i !== index) return todo
64-
return { ...todo, completed: !todo.completed }
65-
})
66-
}
99+
If you choose to write reducers in this style, make sure to learn about the [pitfalls mentioned in the immer docs](https://github.com/mweststrate/immer#pitfalls) . Most importantly, you need to ensure that you either mutate the `state` argument or return a new state, _but not both_. For example, the following reducer would throw an exception if a `toggleTodo` action is passed:
67100

68-
const todosReducer = createReducer([], {
69-
ADD_TODO: addTodo,
70-
TOGGLE_TODO: toggleTodo
101+
```js
102+
const todosReducer = createReducr([], {
103+
[toggleTodo]: (state, action) => {
104+
const index = action.payload
105+
const todo = state[index]
106+
107+
// This case reducer both mutates the passed-in state...
108+
todo.completed = !todo.completed
109+
110+
// ... and returns a new value. This will throw an
111+
// exception. In this example, the easiest fix is
112+
// to remove the `return` statement.
113+
return [
114+
...state.slice(0, index),
115+
todo,
116+
...state.slice(index + 1)
117+
]
118+
}
71119
})
72120
```

0 commit comments

Comments
 (0)