createReducer

createReducer()

Overview

A utility that simplifies creating Redux reducer functions. It uses Immer internally to drastically simplify immutable update logic by writing "mutative" code in your reducers, and supports directly mapping specific action types to case reducer functions that will update the state when that action is dispatched.

Redux reducers are often implemented using a switch statement, with one case for every handled action type.

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, value: state.value + 1 }
    case 'decrement':
      return { ...state, value: state.value - 1 }
    case 'incrementByAmount':
      return { ...state, value: state.value + 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 supports two different forms of defining case reducers to handle actions: a "builder callback" notation and a "map object" notation. Both are equivalent, but the "builder callback" notation is preferred.

With createReducer, your reducers instead look like:

Usage with the "Builder Callback" Notation

overloadSummary

The recommended way of using createReducer is the builder callback notation, as it works best with TypeScript and most IDEs.

Parameters

params

Example Usage

examples

Builder Methods

builder.addCase

summary,remarks

Parameters

params,examples

builder.addMatcher

summary,remarks

Parameters

params,examples

builder.addDefaultCase

summary,remarks

Parameters

params,examples

Usage with the "Map Object" Notation

overloadSummary

While this notation is a bit shorter, it works only in JavaScript, not TypeScript and has less integration with IDEs, so we recommend the "builder callback" notation in most cases.

Parameters

params

Example Usage

examples

Matchers and Default Cases as Arguments

The most readable approach to define matcher cases and default cases is by using the builder.addMatcher and builder.addDefaultCase methods described above, but it is also possible to use these with the object notation by passing an array of {matcher, reducer} objects as the third argument, and a default case reducer as the fourth argument:

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:

The addTodo reducer is straightforward 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.

Writing "mutating" reducers simplifies the code. It's shorter, there's less indirection, and it eliminates common mistakes made while spreading nested state. However, the use of Immer does add some "magic", and Immer has its own nuances in behavior. You should read through 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:

Multiple Case Reducer Execution

Originally, createReducer always matched a given action type to a single case reducer, and only that one case reducer would execute for a given action.

Using action matchers changes that behavior, as multiple matchers may handle a single action.

For any dispatched action, the behavior is:

  • If there is an exact match for the action type, the corresponding case reducer will execute first

  • Any matchers that return true will execute in the order they were defined

  • If a default case reducer is provided, and no case or matcher reducers ran, the default case reducer will execute

  • If no case or matcher reducers ran, the original existing state value will be returned unchanged

The executing reducers form a pipeline, and each of them will receive the output of the previous reducer:

Logging Draft State Values

It's very common for a developer to call console.log(state) during the development process. However, browsers display Proxies in a format that is hard to read, which can make console logging of Immer-based state difficult.

When using either createSlice or createReducer, you may use the current utility that we re-export from the immer library. This utility creates a separate plain copy of the current Immer Draft state value, which can then be logged for viewing as normal.

Last updated

Was this helpful?