Skip to content

Commit 1ac4380

Browse files
committed
Merge branch 'master' of github.com:reduxjs/redux-starter-kit
2 parents 1d3f93e + 1d98748 commit 1ac4380

File tree

1 file changed

+90
-42
lines changed

1 file changed

+90
-42
lines changed

docs/api/createReducer.md

Lines changed: 90 additions & 42 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-
) {}
12+
Redux [reducers](https://redux.js.org/basics/reducers) are often implemented using a `switch` statement, with one `case` for every handled action type.
13+
14+
```js
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+
}
1725
```
1826

19-
Example usage:
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.
28+
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.
2030

2131
```js
22-
import { createReducer } from 'redux-starter-kit'
32+
const counterReducer = createReducer(0, {
33+
increment: (state, action) => state + action.payload,
34+
decrement: (state, action) => state - action.payload
35+
})
36+
```
2337

24-
function addTodo(state, action) {
25-
const { newTodo } = action.payload
38+
If you created action creators using `createAction()`, you can use those directly as keys for the case reducers.
2639

27-
// Can safely call state.push() here
28-
state.push({ ...newTodo, completed: false })
29-
}
40+
```js
41+
const increment = createAction('increment')
42+
const decrement = createAction('decrement')
43+
44+
const counterReducer = createReducer(0, {
45+
[increment]: (state, action) => state + action.payload,
46+
[decrement]: (state, action) => state - action.payload
47+
})
48+
```
3049

31-
function toggleTodo(state, action) {
32-
const { index } = action.payload
50+
## Direct State Mutation
3351

34-
const todo = state[index]
35-
// Can directly modify the todo object
36-
todo.completed = !todo.completed
37-
}
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:
53+
54+
```js
55+
const addTodo = createAction('todos/add')
56+
const toggleTodo = createAction('todos/toggle')
3857

3958
const todosReducer = createReducer([], {
40-
ADD_TODO: addTodo,
41-
TOGGLE_TODO: toggleTodo
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+
}
4272
})
4373
```
4474

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:
46-
47-
```js
48-
import { createReducer } from 'redux-starter-kit'
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.
4976

50-
function addTodo(state, action) {
51-
const { newTodo } = action.payload
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.
5278

53-
// Updates the state immutably without relying on immer
54-
return [...state, { ...newTodo, completed: false }]
55-
}
79+
```js
80+
const addTodo = createAction('todos/add')
81+
const toggleTodo = createAction('todos/toggle')
5682

57-
function toggleTodo(state, action) {
58-
const { index } = action.payload
83+
const todosReducer = createReducer([], {
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

101+
```js
68102
const todosReducer = createReducer([], {
69-
ADD_TODO: addTodo,
70-
TOGGLE_TODO: toggleTodo
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)