createReducer()
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.
Redux reducers are often implemented using a switch
statement, with one case
for every handled action type.
function counterReducer(state = 0, action) { switch (action.type) { case 'increment': return state + action.payload case 'decrement': return state - action.payload default: return state }}
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.
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.
const counterReducer = createReducer(0, { increment: (state, action) => state + action.payload, decrement: (state, action) => state - action.payload})
Action creators that were generated using createAction
may be used directly as the keys here, using
computed property syntax. (If you are using TypeScript, you may have to use actionCreator.type
or actionCreator.toString()
to force the TS compiler to accept the computed property.)
const increment = createAction('increment')const decrement = createAction('decrement')
const counterReducer = createReducer(0, { [increment]: (state, action) => state + action.payload, [decrement.type]: (state, action) => state - action.payload})
The "builder callback" API
Instead of using a simple object as an argument to createReducer
, you can also provide a callback that receives an ActionReducerMapBuilder
instance:
createReducer(0, builder => builder.addCase(increment, (state, action) => { // action is inferred correctly here }))
This is intended for use with TypeScript, as passing a plain object full of reducer functions cannot infer their types correctly in this case. It has no real benefit when used with plain JS.
We recommend using this API if stricter type safety is necessary when defining reducer argument objects.
Direct State Mutation
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:
const addTodo = createAction('todos/add')const toggleTodo = createAction('todos/toggle')
const todosReducer = createReducer([], { [addTodo]: (state, action) => { const todo = action.payload return [...state, todo] }, [toggleTodo]: (state, action) => { const index = action.payload const todo = state[index] return [ ...state.slice(0, index), { ...todo, completed: !todo.completed } ...state.slice(index + 1) ] }})
The addTodo
reducer is pretty easy to follow if you know the ES6 spread syntax. However, the code for toggleTodo
is much less straightforward, especially considering that it only sets a single flag.
To make things easier, createReducer
uses 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.
const addTodo = createAction('todos/add')const toggleTodo = createAction('todos/toggle')
const todosReducer = createReducer([], { [addTodo]: (state, action) => { // This push() operation gets translated into the same // extended-array creation as in the previous example. const todo = action.payload state.push(todo) }, [toggleTodo]: (state, action) => { // The "mutating" version of this case reducer is much // more direct than the explicitly pure one. const index = action.payload const todo = state[index] todo.completed = !todo.completed }})
If you choose to write reducers in this style, make sure to learn about the pitfalls mentioned in the immer docs . 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:
const todosReducer = createReducer([], { [toggleTodo]: (state, action) => { const index = action.payload const todo = state[index]
// This case reducer both mutates the passed-in state... todo.completed = !todo.completed
// ... and returns a new value. This will throw an // exception. In this example, the easiest fix is // to remove the `return` statement. return [...state.slice(0, index), todo, ...state.slice(index + 1)] }})