Repository: reduxjs/redux-toolkit
Pull request #610 (2020-06-13 17:51:32)

add `addMatcher` to builder notations & `actionMatchers` argumen…
Still experimenting with some types & this will need some additional documentation, but I already want to put this through the CI.

The idea behind this is to enable usages such as this:

```ts
type Status = 'pending' | 'rejected' | 'fulfilled';

// Type guard function
function isPendingAction(action: AnyAction): action is PendingAction {
  return action.type.endsWith('/pending') && 'requestId'  in action.meta
};

const thunkStateSlice = createSlice({
  name: "thunkState",
  initialState: {} as {[requestId: string]: Status},
  reducers: {
    reset(){ return {} }
  },
  extraReducers: builder => builder
    // kind of "wildcard-y" extraReducer with a guard function - action is automatically inferred as `PendingAction` due to the signature of `isPendingAction`
    .addMatcher(isPendingAction, (state, action) => {
      state[action.meta.requestId] = 'pending'
    })
})
```

netlify[bot]: Deploy preview for *redux-starter-kit-docs* ready!

Built with commit 1633283bc2a8078b5618553f07ee033999d886f9

https://deploy-preview-610--redux-starter-kit-docs.netlify.app
codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/reduxjs/redux-toolkit/pr/610/builds/32223) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit d2bd259ebd38c27b2b7a2cd172acff3ea6b37701:

|Sandbox| Source |
|--|--|
|[silly-shirley-qdteb](https://codesandbox.io/s/silly-shirley-qdteb)| Configuration |
|[confident-shadow-3zfq6](https://codesandbox.io/s/confident-shadow-3zfq6)| Configuration |
|[summer-pine-uycmt](https://codesandbox.io/s/summer-pine-uycmt)| Configuration |

phryneas: And we have `addDefaultCase`. I'd leave it at that for now. @msutkowski if you can take a look at docs later, that would be great - otherwise I'll try & write some docs tomorrow.

Patch:
diff --git a/docs/api/createAsyncThunk.md b/docs/api/createAsyncThunk.md
index 08a8202f..c5b00bae 100644
--- a/docs/api/createAsyncThunk.md
+++ b/docs/api/createAsyncThunk.md
@@ -195,7 +195,7 @@ const reducer1 = createReducer(initialState, {
   [fetchUserById.fulfilled]: (state, action) => {}
 })
 
-const reducer2 = createReducer(initialState, build => {
+const reducer2 = createReducer(initialState, builder => {
   builder.addCase(fetchUserById.fulfilled, (state, action) => {})
 })
 
diff --git a/docs/api/createReducer.md b/docs/api/createReducer.md
index caf93b05..5196f016 100644
--- a/docs/api/createReducer.md
+++ b/docs/api/createReducer.md
@@ -7,6 +7,8 @@ hide_title: true
 
 # `createReducer()`
 
+## Overview
+
 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](https://redux.js.org/basics/reducers) are often implemented using a `switch` statement, with one `case` for every handled action type.
@@ -24,9 +26,19 @@ function counterReducer(state = 0, action) {
 }
 ```
 
-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.
+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.
+
+## Parameters
+
+`createReducer` accepts four possible parameters, with the first two required.
+
+### `initialState`
+
+The initial state that should be used when the reducer is called the first time.
 
-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.
+### `caseReducers`
+
+An object mapping from action types to _case reducers_, each of which handles one specific action type.
 
 ```js
 const counterReducer = createReducer(0, {
@@ -36,10 +48,7 @@ const counterReducer = createReducer(0, {
 ```
 
 Action creators that were generated using [`createAction`](./createAction.md) may be used directly as the keys here, using
-computed property syntax.
-
-> **Note**: If you are using TypeScript, we recommend using the `builder callback` API that is shown below. If you do not use the `builder callback` and are using TypeScript, you will need to use `actionCreator.type` or `actionCreator.toString()`
-> to force the TS compiler to accept the computed property. Please see [Usage With TypeScript](./../usage/usage-with-typescript.md#type-safety-with-extraReducers) for further details.
+computed property syntax:
 
 ```js
 const increment = createAction('increment')
@@ -51,21 +60,43 @@ const counterReducer = createReducer(0, {
 })
 ```
 
-### The "builder callback" API
+Alternately, the second argument may be a "builder callback" function that can be used to add case handlers for specific action types, match against a range of action types, or provide a fallback default case if no other actions matched:
 
-Instead of using a simple object as an argument to `createReducer`, you can also provide a callback that receives an `ActionReducerMapBuilder` instance:
+```js
+const initialState = {
+  counter: 0,
+  rejectedActions: 0,
+  unhandledActions: 0
+}
 
-```typescript
-createReducer(0, builder =>
-  builder.addCase(increment, (state, action) => {
-    // action is inferred correctly here
-  })
-)
+const exampleReducer = createReducer(initialState, builder => {
+  builder
+    .addCase('counter', state => {
+      state.counter++
+    })
+    .addMatcher(
+      action => action.endsWith('/rejected'),
+      (state, action) => {
+        state.rejectedActions++
+      }
+    )
+    .addDefaultCase((state, action) => {
+      state.unhandledActions++
+    })
+})
 ```
 
-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.
+See [the `builder callback` API](#the-builder-callback-api) below for details on defining reducers using this syntax.
 
-We recommend using this API if stricter type safety is necessary when defining reducer argument objects.
+> **Note**: If you are using TypeScript, we specifically recommend using the builder callback API to get proper inference of TS types for action objects. If you do not use the builder callback and are using TypeScript, you will need to use `actionCreator.type` or `actionCreator.toString()` as the key to force the TS compiler to accept the computed property. Please see [Usage With TypeScript](./../usage/usage-with-typescript.md#type-safety-with-extraReducers) for further details.
+
+### `actionMatchers`
+
+An optional array of objects that include a `matcher` function to determine if an action should be handled, and a `reducer` function that updates the state. This argument will be ignored if the second argument is a builder callback.
+
+### `defaultCase`
+
+A reducer that will be run if no other case reducers or matchers handle a given action. This argument will be ignored if the second argument is a builder callback.
 
 ## Direct State Mutation
 
@@ -92,7 +123,7 @@ const todosReducer = createReducer([], {
 })
 ```
 
-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.
+The `addTodo` reducer is straightforward 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.
 
 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.
 
@@ -117,7 +148,7 @@ const todosReducer = createReducer([], {
 })
 ```
 
-If you choose to write reducers in this style, make sure to learn about the [pitfalls mentioned in the immer docs](https://immerjs.github.io/immer/docs/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:
+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](https://immerjs.github.io/immer/docs/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:
 
 ```js
 const todosReducer = createReducer([], {
@@ -136,11 +167,163 @@ const todosReducer = createReducer([], {
 })
 ```
 
+## The "builder callback" API
+
+Instead of using a plain object as an argument to `createReducer`, you can also provide a "builder callback" function that receives an `ActionReducerMapBuilder` instance:
+
+```typescript
+const increment = createAction('increment')
+const decrement = createAction('decrement')
+createReducer(0, builder =>
+  builder
+    .addCase(increment, (state, action) => {
+      // action is inferred correctly here
+    })
+    // You can chain calls, or have separate `builder.addCase()` lines each time
+    .addCase(decrement, (state, action) => {})
+    // You can match a range of action types
+    .addMatcher(
+      action => action.endsWith('rejected'),
+      (state, action) => {}
+    )
+    // and provide a default case if no other handlers matched
+    .addDefaultCase((state, action) => {})
+)
+```
+
+While the object syntax is shorter, the builder callback syntax allows adding multiple forms of reducers. It also provides better type inference, as passing a plain object full of reducer functions cannot infer their types correctly in this case.
+
+We recommend using this API if stricter type safety is necessary when defining reducer argument objects.
+
+### `builder.addCase`
+
+Adds a case reducer to handle a single exact action type. The first argument may be either a plain action type string, or an action creator generated by [`createAction`](./createAction.md) that can be used to determine the action type. The second argument is the actual case reducer function.
+
+All calls to `builder.addCase` must come before any calls to `builder.addMatcher` or `builder.addDefaultCase`.
+
+### `builder.addMatcher`
+
+`builder.addMatcher` allows you to match your reducer against your own filter function instead of only the `action.type` property.
+This allows for a lot of generic behaviour, so you could for example write a "generic loading tracker" state based on an approach like this:
+
+```js
+const initialState = {}
+const resetAction = createAction('reset-tracked-loading-state')
+const reducer = createReducer(initialState, builder => {
+  builder
+    .addCase(resetAction, () => initialState)
+    .addMatcher(
+      action => action.type.endsWith('/pending') && 'requestId' in action.meta,
+      (state, action) => {
+        state[action.meta.requestId] = 'pending'
+      }
+    )
+    .addMatcher(
+      action => action.type.endsWith('/rejected') && 'requestId' in action.meta,
+      (state, action) => {
+        state[action.meta.requestId] = 'rejected'
+      }
+    )
+    .addMatcher(
+      action =>
+        action.type.endsWith('/fulfilled') && 'requestId' in action.meta,
+      (state, action) => {
+        state[action.meta.requestId] = 'fulfilled'
+      }
+    )
+})
+```
+
+Note that _all_ matching matcher reducers will execute in order, even if a case reducer has already executed.
+
+### `builder.addDefaultCase`
+
+`builder.addDefaultCase` allows you to add a "default" reducer that will execute if no case reducer or matcher reducer was executed.
+
+```js
+const reducer = createReducer(initialState, builder => {
+  builder
+    // .addCase
+    // ...
+    // .addMatcher
+    // ...
+    .addDefaultCase((state, action) => {
+      state.otherActions++
+    })
+})
+```
+
+### 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:
+
+```js
+const isStringPayloadAction = action => typeof action.payload === 'string'
+
+const lengthOfAllStringsReducer = createReducer(
+  // initial state
+  { strLen: 0, nonStringActions: 0 },
+  // normal reducers
+  {
+    /*...*/
+  },
+  //  array of matcher reducers
+  [
+    {
+      matcher: isStringPayloadAction,
+      reducer(state, action) {
+        state.strLen += action.payload.length
+      }
+    }
+  ],
+  // default reducer
+  state => {
+    state.nonStringActions++
+  }
+)
+```
+
+## 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.
+
+The action matcher support 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:
+
+```js
+const reducer = createReducer(0, builder => {
+  builder
+    .addCase('increment', state => state + 1)
+    .addMatcher(
+      action => action.type.startsWith('i'),
+      state => state * 5
+    )
+    .addMatcher(
+      action => action.type.endsWith('t'),
+      state => state + 2
+    )
+})
+
+console.log(reducer(0, { type: 'increment' }))
+// Returns 7, as the 'increment' case and both matchers all ran in sequence:
+// - case 'increment": 0 => 1
+// - matcher starts with 'i': 1 => 5
+// - matcher ends with 't': 5 => 7
+```
+
 ## Debugging your state
 
 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`](./otherExports#current.md) utility that we re-export from the [`immer` library](https://immerjs.github.io/immer). This utility creates a separate plain copy of the current Immer `Draft` state value, which can then be logged for viewing as normal.
+When using either `createSlice` or `createReducer`, you may use the [`current`](./otherExports#current.md) utility that we re-export from the [`immer` library](https://immerjs.github.io/immer/docs/current). This utility creates a separate plain copy of the current Immer `Draft` state value, which can then be logged for viewing as normal.
 
 ```ts
 // todosSlice.js
diff --git a/docs/api/createSlice.md b/docs/api/createSlice.md
index bcc17c12..5e5cf8bf 100644
--- a/docs/api/createSlice.md
+++ b/docs/api/createSlice.md
@@ -16,19 +16,28 @@ and automatically generates action creators and action types that correspond to
 
 ```ts
 function createSlice({
+      // A name, used in action types
+    name: string,
+        // The initial state for the reducer
+    initialState: any,
     // An object of "case reducers". Key names will be used to generate actions.
     reducers: Object<string, ReducerFunction | ReducerAndPrepareObject>
-    // The initial state for the reducer
-    initialState: any,
-    // A name, used in action types
-    name: string,
-    // An additional object of "case reducers". Keys should be other action types.
+    // An additional object of "case reducers", where the keys should be other
+    // action types, or a "builder callback" function used to add more reducers
     extraReducers?:
     | Object<string, ReducerFunction>
     | ((builder: ActionReducerMapBuilder<State>) => void)
 })
 ```
 
+### `initialState`
+
+The initial state value for this slice of state.
+
+### `name`
+
+A string name for this slice of state. Generated action type constants will use this as a prefix.
+
 ### `reducers`
 
 An object containing Redux "case reducer" functions (functions intended to handle a specific action type, equivalent
@@ -42,17 +51,38 @@ descriptive names.
 This object will be passed to [`createReducer`](./createReducer.md), so the reducers may safely "mutate" the
 state they are given.
 
-#### Customizing Generated Action Creators
-
-If you need to customize the creation of the payload value of an action creator by means of a [`prepare callback`](./createAction.md#using-prepare-callbacks-to-customize-action-contents), the value of the appropriate field of the `reducers` argument object should be an object instead of a function. This object must contain two properties: `reducer` and `prepare`. The value of the `reducer` field should be the case reducer function while the value of the `prepare` field should be the prepare callback function.
-
-### `initialState`
+```js
+const counterSlice = createSlice({
+  name: 'counter',
+  initialState: 0,
+  reducers: {
+    increment: state => state + 1
+  }
+})
+// Will handle the action type `'counter/increment'`
+```
 
-The initial state value for this slice of state.
+#### Customizing Generated Action Creators
 
-### `name`
+If you need to customize the creation of the payload value of an action creator by means of a [`prepare callback`](./createAction.md#using-prepare-callbacks-to-customize-action-contents), the value of the appropriate field of the `reducers` argument object should be an object instead of a function. This object must contain two properties: `reducer` and `prepare`. The value of the `reducer` field should be the case reducer function while the value of the `prepare` field should be the prepare callback function:
 
-A string name for this slice of state. Generated action type constants will use this as a prefix.
+```js
+const todosSlice = createSlice({
+  name: 'todos',
+  initialState: [],
+  reducers: {
+    addTodo: {
+      reducer: (state, action) => {
+        state.push(action.payload)
+      },
+      prepare: text => {
+        const id = nanoid()
+        return { payload: { id, text } }
+      }
+    }
+  }
+})
+```
 
 ### `extraReducers`
 
@@ -60,11 +90,11 @@ One of the key concepts of Redux is that each slice reducer "owns" its slice of
 can independently respond to the same action type. `extraReducers` allows `createSlice` to respond to other action types
 besides the types it has generated.
 
-Like `reducers`, `extraReducers` should be an object containing Redux case reducer functions. However, the keys should
+Like `reducers`, `extraReducers` can be an object containing Redux case reducer functions. However, the keys should
 be other Redux string action type constants, and `createSlice` will _not_ auto-generate action types or action creators
 for reducers included in this parameter.
 
-As with `reducers`, these reducers will also be passed to `createReducer` and may "mutate" their state safely.
+As with `reducers`, these case reducers will also be passed to `createReducer` and may "mutate" their state safely.
 
 If two fields from `reducers` and `extraReducers` happen to end up with the same action type string,
 the function from `reducers` will be used to handle that action type.
@@ -82,7 +112,8 @@ createSlice({
   extraReducers: {
     [incrementBy]: (state, action) => {
       return state + action.payload
-    }
+    },
+    'some/other/action': (state, action) => {}
   }
 })
 ```
@@ -92,25 +123,38 @@ createSlice({
 
 ### The "builder callback" API for `extraReducers`
 
-Instead of using a simple object as `extraReducers`, you can also use a callback that receives a `ActionReducerMapBuilder` instance.
+Instead of using an object as `extraReducers`, you can also use a callback that receives a `ActionReducerMapBuilder` instance.
+
+This builder notation is also the only way to add matcher reducers and default case reducers to your slice.
 
 ```typescript
 const incrementBy = createAction<number>('incrementBy')
+const decrement = createAction('decrement')
 
 createSlice({
   name: 'counter',
   initialState: 0,
   reducers: {},
-  extraReducers: builder => {
-    builder.addCase(incrementBy, (state, action) => {
-      // action is inferred correctly here with `action.payload` as a `number`
-      return state + action.payload
-    })
-  }
+  extraReducers: builder =>
+    builder
+      .addCase(incrementBy, (state, action) => {
+        // action is inferred correctly here if using TS
+      })
+      // You can chain calls, or have separate `builder.addCase()` lines each time
+      .addCase(decrement, (state, action) => {})
+      // You can match a range of action types
+      .addMatcher(
+        action => action.endsWith('rejected'),
+        (state, action) => {}
+      )
+      // and provide a default case if no other handlers matched
+      .addDefaultCase((state, action) => {})
 })
 ```
 
-We recommend using this API if stricter type safety is necessary when defining reducer argument objects. It's particularly useful for working with actions produced by `createAction` and `createAsyncThunk`.
+We recommend using this API if stricter type safety is necessary when defining reducer argument objects, as it will correctly infer the action type in the reducer based on the provided action creator. It's particularly useful for working with actions produced by `createAction` and `createAsyncThunk`.
+
+See [the "builder callback API" section of the `createReducer` reference](./createReducer.md#the-builder-callback-api) for details on how to use `builder.addCase`, `builder.addMatcher`, and `builder.addDefault`
 
 ## Return Value
 
@@ -120,7 +164,8 @@ We recommend using this API if stricter type safety is necessary when defining r
 {
     name : string,
     reducer : ReducerFunction,
-    actions : Object<string, ActionCreator>,
+    actions : Record<string, ActionCreator>,
+    caseReducers: Record<string, CaseReducer>
 }
 ```
 
diff --git a/docs/usage/usage-guide.md b/docs/usage/usage-guide.md
index 86ce1d83..15a76ad4 100644
--- a/docs/usage/usage-guide.md
+++ b/docs/usage/usage-guide.md
@@ -1021,3 +1021,18 @@ export const usersAdapter = createEntityAdapter({
   sortComparer: (a, b) => a.first_name.localeCompare(b.first_name)
 })
 ```
+
+## Working with Non-serializable Data
+
+Occasionally, you may have actions that need to accept non-serializable data. This should be done very rarely and only if necessary. These non-serializable payloads shouldn't ever make it into your application state through a reducer. However, if you're using libraries such as redux saga or you're using middleware that could reasonably use non-serializable data you'll want to do the following:
+
+```js
+configureStore({
+  //...
+  middleware: getDefaultMiddleware({
+    serializableCheck: {
+      ignoredActions: ['your/action']
+    }
+  })
+})
+```
diff --git a/docs/usage/usage-with-typescript.md b/docs/usage/usage-with-typescript.md
index b3320486..a8d3017c 100644
--- a/docs/usage/usage-with-typescript.md
+++ b/docs/usage/usage-with-typescript.md
@@ -214,6 +214,23 @@ createReducer(0, builder =>
 
 We recommend using this API if stricter type safety is necessary when defining reducer argument objects.
 
+#### Typing `builder.addMatcher`
+
+As the first `matcher` argument to `builder.addMatcher`, a [type predicate](https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates) function should be used.
+As a result, the `action` argument for the second `reducer` argument can be inferred by TypeScript:
+
+```ts
+function isNumberValueAction(action: AnyAction): Action is PayloadAction<{ value: number }> {
+  return typeof action.payload.value === 'number'
+}
+
+createReducer({ value: 0 }, builder =>
+   builder.addMatcher(isNumberValueAction, (state, action) => {
+      state.value += action.payload.value
+   })
+})
+```
+
 ## `createSlice`
 
 As `createSlice` creates your actions as well as your reducer for you, you don't have to worry about type safety here.
@@ -372,6 +389,8 @@ const usersSlice = createSlice({
 })
 ```
 
+Like the `builder` in `createReducer`, this `builder` also accepts `addMatcher` (see [typing `builder.matcher`](#typing-builderaddmatcher)) and `addDefaultCase`.
+
 ### Wrapping `createSlice`
 
 If you need to reuse reducer logic, it is common to write ["higher-order reducers"](https://redux.js.org/recipes/structuring-reducers/reusing-reducer-logic#customizing-behavior-with-higher-order-reducers) that wrap a reducer function with additional common behavior. This can be done with `createSlice` as well, but due to the complexity of the types for `createSlice`, you have to use the `SliceCaseReducers` and `ValidateSliceCaseReducers` types in a very specific way.
diff --git a/etc/redux-toolkit.api.md b/etc/redux-toolkit.api.md
index 7c91aca3..a1c53745 100644
--- a/etc/redux-toolkit.api.md
+++ b/etc/redux-toolkit.api.md
@@ -55,6 +55,8 @@ export interface ActionCreatorWithPreparedPayload<Args extends unknown[], P, T e
 export interface ActionReducerMapBuilder<State> {
     addCase<ActionCreator extends TypedActionCreator<string>>(actionCreator: ActionCreator, reducer: CaseReducer<State, ReturnType<ActionCreator>>): ActionReducerMapBuilder<State>;
     addCase<Type extends string, A extends Action<Type>>(type: Type, reducer: CaseReducer<State, A>): ActionReducerMapBuilder<State>;
+    addDefaultCase(reducer: CaseReducer<State, AnyAction>): {};
+    addMatcher<A extends AnyAction>(matcher: ActionMatcher<A>, reducer: CaseReducer<State, A>): Omit<ActionReducerMapBuilder<State>, 'addCase'>;
 }
 
 // @public @deprecated
@@ -147,7 +149,7 @@ export function createImmutableStateInvariantMiddleware(options?: ImmutableState
 export { createNextState }
 
 // @public
-export function createReducer<S, CR extends CaseReducers<S, any> = CaseReducers<S, any>>(initialState: S, actionsMap: CR): Reducer<S>;
+export function createReducer<S, CR extends CaseReducers<S, any> = CaseReducers<S, any>>(initialState: S, actionsMap: CR, actionMatchers?: ActionMatcherDescriptionCollection<S>, defaultCaseReducer?: CaseReducer<S>): Reducer<S>;
 
 // @public
 export function createReducer<S>(initialState: S, builderCallback: (builder: ActionReducerMapBuilder<S>) => void): Reducer<S>;
diff --git a/package-lock.json b/package-lock.json
index e5eeac51..7e9c4362 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4563,9 +4563,9 @@
       "dev": true
     },
     "immer": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.1.tgz",
-      "integrity": "sha512-DpWL/ES2Ba8KwW+A5AgNRoJPZP8LxdHejKXar1VfbF5a7ATvvekzmmNSQxje+WG0DIhuanvFEumFffVqyCLmBQ=="
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.3.tgz",
+      "integrity": "sha512-1f/jaF27WVb3X5Tk1AmOYZE+JZgMl50pIS5I8e/Je+HpYHjHwPfzm6dTEvOAUi6jq+/anFXev7agoOgJaL5RFw=="
     },
     "import-fresh": {
       "version": "3.2.1",
diff --git a/package.json b/package.json
index a6422b21..b4167a8e 100644
--- a/package.json
+++ b/package.json
@@ -58,7 +58,7 @@
     "src"
   ],
   "dependencies": {
-    "immer": "7.0.1",
+    "immer": "^7.0.3",
     "redux": "^4.0.0",
     "redux-thunk": "^2.3.0",
     "reselect": "^4.0.0"
diff --git a/src/createReducer.ts b/src/createReducer.ts
index c516a6c6..5fe7784b 100644
--- a/src/createReducer.ts
+++ b/src/createReducer.ts
@@ -4,6 +4,7 @@ import {
   executeReducerBuilderCallback,
   ActionReducerMapBuilder
 } from './mapBuilders'
+import { NoInfer } from './tsHelpers'
 
 /**
  * Defines a mapping from action types to corresponding action object shapes.
@@ -15,6 +16,19 @@ import {
  */
 export type Actions<T extends keyof any = string> = Record<T, Action>
 
+export interface ActionMatcher<A extends AnyAction> {
+  (action: AnyAction): action is A
+}
+
+export type ActionMatcherDescription<S, A extends AnyAction> = {
+  matcher: ActionMatcher<A>
+  reducer: CaseReducer<S, NoInfer<A>>
+}
+
+export type ActionMatcherDescriptionCollection<S> = Array<
+  ActionMatcherDescription<S, any>
+>
+
 /**
  * An *case reducer* is a reducer function for a specific action type. Case
  * reducers can be composed to full reducers using `createReducer()`.
@@ -64,13 +78,22 @@ export type CaseReducers<S, AS extends Actions> = {
  * @param initialState The initial state to be returned by the reducer.
  * @param actionsMap A mapping from action types to action-type-specific
  *   case reducers.
+ * @param actionMatchers An array of matcher definitions in the form `{matcher, reducer}`.
+ *   All matching reducers will be executed in order, independently if a case reducer matched or not.
+ * @param defaultCaseReducer A "default case" reducer that is executed if no case reducer and no matcher
+ *   reducer was executed for this action.
  *
  * @public
  */
 export function createReducer<
   S,
   CR extends CaseReducers<S, any> = CaseReducers<S, any>
->(initialState: S, actionsMap: CR): Reducer<S>
+>(
+  initialState: S,
+  actionsMap: CR,
+  actionMatchers?: ActionMatcherDescriptionCollection<S>,
+  defaultCaseReducer?: CaseReducer<S>
+): Reducer<S>
 /**
  * A utility function that allows defining a reducer as a mapping from action
  * type to *case reducer* functions that handle these action types. The
@@ -97,50 +120,63 @@ export function createReducer<S>(
   initialState: S,
   mapOrBuilderCallback:
     | CaseReducers<S, any>
-    | ((builder: ActionReducerMapBuilder<S>) => void)
+    | ((builder: ActionReducerMapBuilder<S>) => void),
+  actionMatchers: ActionMatcherDescriptionCollection<S> = [],
+  defaultCaseReducer?: CaseReducer<S>
 ): Reducer<S> {
-  let actionsMap =
+  let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
     typeof mapOrBuilderCallback === 'function'
       ? executeReducerBuilderCallback(mapOrBuilderCallback)
-      : mapOrBuilderCallback
+      : [mapOrBuilderCallback, actionMatchers, defaultCaseReducer]
 
   return function(state = initialState, action): S {
-    const caseReducer = actionsMap[action.type]
-    if (caseReducer) {
-      if (isDraft(state)) {
-        // If it's already a draft, we must already be inside a `createNextState` call,
-        // likely because this is being wrapped in `createReducer`, `createSlice`, or nested
-        // inside an existing draft. It's safe to just pass the draft to the mutator.
-        const draft = state as Draft<S> // We can assume this is already a draft
-        const result = caseReducer(draft, action)
+    let caseReducers = [
+      actionsMap[action.type],
+      ...finalActionMatchers
+        .filter(({ matcher }) => matcher(action))
+        .map(({ reducer }) => reducer)
+    ]
+    if (caseReducers.filter(cr => !!cr).length === 0) {
+      caseReducers = [finalDefaultCaseReducer]
+    }
 
-        if (typeof result === 'undefined') {
-          return state
-        }
+    return caseReducers.reduce((previousState, caseReducer): S => {
+      if (caseReducer) {
+        if (isDraft(previousState)) {
+          // If it's already a draft, we must already be inside a `createNextState` call,
+          // likely because this is being wrapped in `createReducer`, `createSlice`, or nested
+          // inside an existing draft. It's safe to just pass the draft to the mutator.
+          const draft = previousState as Draft<S> // We can assume this is already a draft
+          const result = caseReducer(draft, action)
 
-        return result
-      } else if (!isDraftable(state)) {
-        // If state is not draftable (ex: a primitive, such as 0), we want to directly
-        // return the caseReducer func and not wrap it with produce.
-        const result = caseReducer(state as any, action)
+          if (typeof result === 'undefined') {
+            return previousState
+          }
 
-        if (typeof result === 'undefined') {
-          throw Error(
-            'A case reducer on a non-draftable value must not return undefined'
-          )
-        }
+          return result
+        } else if (!isDraftable(previousState)) {
+          // If state is not draftable (ex: a primitive, such as 0), we want to directly
+          // return the caseReducer func and not wrap it with produce.
+          const result = caseReducer(previousState as any, action)
 
-        return result
-      } else {
-        // @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
-        // than an Immutable<S>, and TypeScript cannot find out how to reconcile
-        // these two types.
-        return createNextState(state, (draft: Draft<S>) => {
-          return caseReducer(draft, action)
-        })
+          if (typeof result === 'undefined') {
+            throw Error(
+              'A case reducer on a non-draftable value must not return undefined'
+            )
+          }
+
+          return result
+        } else {
+          // @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
+          // than an Immutable<S>, and TypeScript cannot find out how to reconcile
+          // these two types.
+          return createNextState(previousState, (draft: Draft<S>) => {
+            return caseReducer(draft, action)
+          })
+        }
       }
-    }
 
-    return state
+      return previousState
+    }, state)
   }
 }
diff --git a/src/createSlice.ts b/src/createSlice.ts
index c3b761a4..d1929dbb 100644
--- a/src/createSlice.ts
+++ b/src/createSlice.ts
@@ -12,7 +12,7 @@ import {
   ActionReducerMapBuilder,
   executeReducerBuilderCallback
 } from './mapBuilders'
-import { Omit } from './tsHelpers'
+import { Omit, NoInfer } from './tsHelpers'
 
 /**
  * An action creator attached to a slice.
@@ -164,15 +164,6 @@ type SliceDefinedCaseReducers<CaseReducers extends SliceCaseReducers<any>> = {
     : CaseReducers[Type]
 }
 
-/**
- * Helper type. Passes T out again, but boxes it in a way that it cannot
- * "widen" the type by accident if it is a generic that should be inferred
- * from elsewhere.
- *
- * @internal
- */
-type NoInfer<T> = [T][T extends any ? 0 : never]
-
 /**
  * Used on a SliceCaseReducers object.
  * Ensures that if a CaseReducer is a `CaseReducerWithPrepare`, that
@@ -225,12 +216,16 @@ export function createSlice<
     throw new Error('`name` is a required option for createSlice')
   }
   const reducers = options.reducers || {}
-  const extraReducers =
+  const [
+    extraReducers = {},
+    actionMatchers = [],
+    defaultCaseReducer = undefined
+  ] =
     typeof options.extraReducers === 'undefined'
-      ? {}
+      ? []
       : typeof options.extraReducers === 'function'
       ? executeReducerBuilderCallback(options.extraReducers)
-      : options.extraReducers
+      : [options.extraReducers]
 
   const reducerNames = Object.keys(reducers)
 
@@ -260,7 +255,12 @@ export function createSlice<
   })
 
   const finalCaseReducers = { ...extraReducers, ...sliceCaseReducersByType }
-  const reducer = createReducer(initialState, finalCaseReducers as any)
+  const reducer = createReducer(
+    initialState,
+    finalCaseReducers as any,
+    actionMatchers,
+    defaultCaseReducer
+  )
 
   return {
     name,
diff --git a/src/entities/fixtures/book.ts b/src/entities/fixtures/book.ts
index 6191374a..8c0a8d5d 100644
--- a/src/entities/fixtures/book.ts
+++ b/src/entities/fixtures/book.ts
@@ -1,6 +1,7 @@
 export interface BookModel {
   id: string
   title: string
+  author?: string
 }
 
 export const AClockworkOrange: BookModel = Object.freeze({
@@ -17,3 +18,9 @@ export const TheGreatGatsby: BookModel = Object.freeze({
   id: 'tgg',
   title: 'The Great Gatsby'
 })
+
+export const TheHobbit: BookModel = Object.freeze({
+  id: 'th',
+  title: 'The Hobbit',
+  author: 'J. R. R. Tolkien'
+})
diff --git a/src/entities/unsorted_state_adapter.ts b/src/entities/unsorted_state_adapter.ts
index 13f20733..4fec5c1f 100644
--- a/src/entities/unsorted_state_adapter.ts
+++ b/src/entities/unsorted_state_adapter.ts
@@ -16,7 +16,7 @@ export function createUnsortedStateAdapter<T>(
 ): EntityStateAdapter<T> {
   type R = EntityState<T>
 
-  function addOneMutably(entity: T, state: EntityState<T>): void {
+  function addOneMutably(entity: T, state: R): void {
     const key = selectIdValue(entity, selectId)
 
     if (key in state.entities) {
@@ -108,10 +108,15 @@ export function createUnsortedStateAdapter<T>(
       if (update.id in state.entities) {
         // If there are multiple updates to one entity, merge them together
         updatesPerEntity[update.id] = {
+          id: update.id,
           // Spreads ignore falsy values, so this works even if there isn't
           // an existing update already at this key
-          ...updatesPerEntity[update.id],
-          ...update
+          changes: {
+            ...(updatesPerEntity[update.id]
+              ? updatesPerEntity[update.id].changes
+              : null),
+            ...update.changes
+          }
         }
       }
     })
diff --git a/src/immutableStateInvariantMiddleware.ts b/src/immutableStateInvariantMiddleware.ts
index 0455306b..b38c6579 100644
--- a/src/immutableStateInvariantMiddleware.ts
+++ b/src/immutableStateInvariantMiddleware.ts
@@ -242,7 +242,7 @@ export function createImmutableStateInvariantMiddleware(
             result.path || []
           ).join(
             '.'
-          )}'.  This may cause incorrect behavior. (http://redux.js.org/docs/Troubleshooting.html#never-mutate-reducer-arguments)`
+          )}'.  This may cause incorrect behavior. (https://redux.js.org/troubleshooting#never-mutate-reducer-arguments)`
         )
       })
 
@@ -264,7 +264,7 @@ export function createImmutableStateInvariantMiddleware(
               '.'
             )}. Take a look at the reducer(s) handling the action ${stringify(
               action
-            )}. (http://redux.js.org/docs/Troubleshooting.html#never-mutate-reducer-arguments)`
+            )}. (https://redux.js.org/troubleshooting#never-mutate-reducer-arguments)`
           )
       })
 
diff --git a/src/mapBuilders.ts b/src/mapBuilders.ts
index 6a0ea0ab..c7bdfa98 100644
--- a/src/mapBuilders.ts
+++ b/src/mapBuilders.ts
@@ -1,5 +1,10 @@
-import { Action } from 'redux'
-import { CaseReducer, CaseReducers } from './createReducer'
+import { Action, AnyAction } from 'redux'
+import {
+  CaseReducer,
+  CaseReducers,
+  ActionMatcher,
+  ActionMatcherDescriptionCollection
+} from './createReducer'
 
 export interface TypedActionCreator<Type extends string> {
   (...args: any[]): Action<Type>
@@ -30,17 +35,60 @@ export interface ActionReducerMapBuilder<State> {
     type: Type,
     reducer: CaseReducer<State, A>
   ): ActionReducerMapBuilder<State>
+
+  /**
+   * Adds a reducer for all actions, using `matcher` as a filter function.
+   * If multiple matcher reducers match, all of them will be executed in the order
+   * they were defined if - even if a case reducer already matched.
+   * @param matcher A matcher function. In TypeScript, this should be a [type predicate](https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates)
+   *   function
+   * @param reducer
+   */
+  addMatcher<A extends AnyAction>(
+    matcher: ActionMatcher<A>,
+    reducer: CaseReducer<State, A>
+  ): Omit<ActionReducerMapBuilder<State>, 'addCase'>
+
+  /**
+   * Adds a "default case" reducer that is executed if no case reducer and no matcher
+   * reducer was executed for this action.
+   * @param reducer
+   */
+  addDefaultCase(reducer: CaseReducer<State, AnyAction>): {}
 }
 
 export function executeReducerBuilderCallback<S>(
   builderCallback: (builder: ActionReducerMapBuilder<S>) => void
-): CaseReducers<S, any> {
+): [
+  CaseReducers<S, any>,
+  ActionMatcherDescriptionCollection<S>,
+  CaseReducer<S, AnyAction> | undefined
+] {
   const actionsMap: CaseReducers<S, any> = {}
+  const actionMatchers: ActionMatcherDescriptionCollection<S> = []
+  let defaultCaseReducer: CaseReducer<S, AnyAction> | undefined
   const builder = {
     addCase(
       typeOrActionCreator: string | TypedActionCreator<any>,
       reducer: CaseReducer<S>
     ) {
+      if (process.env.NODE_ENV !== 'production') {
+        /*
+         to keep the definition by the user in line with actual behavior, 
+         we enforce `addCase` to always be called before calling `addMatcher`
+         as matching cases take precedence over matchers
+         */
+        if (actionMatchers.length > 0) {
+          throw new Error(
+            '`builder.addCase` should only be called before calling `builder.addMatcher`'
+          )
+        }
+        if (defaultCaseReducer) {
+          throw new Error(
+            '`builder.addCase` should only be called before calling `builder.addDefaultCase`'
+          )
+        }
+      }
       const type =
         typeof typeOrActionCreator === 'string'
           ? typeOrActionCreator
@@ -52,8 +100,31 @@ export function executeReducerBuilderCallback<S>(
       }
       actionsMap[type] = reducer
       return builder
+    },
+    addMatcher<A extends AnyAction>(
+      matcher: ActionMatcher<A>,
+      reducer: CaseReducer<S, A>
+    ) {
+      if (process.env.NODE_ENV !== 'production') {
+        if (defaultCaseReducer) {
+          throw new Error(
+            '`builder.addMatcher` should only be called before calling `builder.addDefaultCase`'
+          )
+        }
+      }
+      actionMatchers.push({ matcher, reducer })
+      return builder
+    },
+    addDefaultCase(reducer: CaseReducer<S, AnyAction>) {
+      if (process.env.NODE_ENV !== 'production') {
+        if (defaultCaseReducer) {
+          throw new Error('`builder.addDefaultCase` can only be called once')
+        }
+      }
+      defaultCaseReducer = reducer
+      return builder
     }
   }
   builderCallback(builder)
-  return actionsMap
+  return [actionsMap, actionMatchers, defaultCaseReducer]
 }
diff --git a/src/serializableStateInvariantMiddleware.ts b/src/serializableStateInvariantMiddleware.ts
index a69aaad2..03bda311 100644
--- a/src/serializableStateInvariantMiddleware.ts
+++ b/src/serializableStateInvariantMiddleware.ts
@@ -176,7 +176,8 @@ export function createSerializableStateInvariantMiddleware(
           value,
           '\nTake a look at the logic that dispatched this action: ',
           action,
-          '\n(See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)'
+          '\n(See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)',
+          '\n(To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)'
         )
       }
     })
diff --git a/src/tsHelpers.ts b/src/tsHelpers.ts
index d624cd35..43ec40a3 100644
--- a/src/tsHelpers.ts
+++ b/src/tsHelpers.ts
@@ -90,4 +90,13 @@ type UnionToIntersection<U> = (U extends any
   ? I
   : never
 
+/**
+ * Helper type. Passes T out again, but boxes it in a way that it cannot
+ * "widen" the type by accident if it is a generic that should be inferred
+ * from elsewhere.
+ *
+ * @internal
+ */
+export type NoInfer<T> = [T][T extends any ? 0 : never]
+
 export type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
diff --git a/website/src/css/custom.css b/website/src/css/custom.css
index 82258b8f..a712b6c5 100644
--- a/website/src/css/custom.css
+++ b/website/src/css/custom.css
@@ -62,6 +62,11 @@ blockquote {
   border-radius: var(--ifm-global-radius);
 }
 
+html[data-theme='dark'] blockquote {
+   border-left-color: var(--ifm-color-primary);
+   color: #000;
+}
+
 .docusaurus-highlight-code-line {
   background-color: rgb(72, 77, 91);
   display: block;


Test patch:
diff --git a/src/createReducer.test.ts b/src/createReducer.test.ts
index ba69815a..a33fff8d 100644
--- a/src/createReducer.test.ts
+++ b/src/createReducer.test.ts
@@ -1,7 +1,7 @@
 import { createReducer, CaseReducer } from './createReducer'
 import { PayloadAction, createAction } from './createAction'
 import { createNextState, Draft } from './'
-import { Reducer } from 'redux'
+import { Reducer, AnyAction } from 'redux'
 
 interface Todo {
   text: string
@@ -106,6 +106,120 @@ describe('createReducer', () => {
     behavesLikeReducer(wrappedReducer)
   })
 
+  describe('actionMatchers argument', () => {
+    const prepareNumberAction = (payload: number) => ({
+      payload,
+      meta: { type: 'number_action' }
+    })
+    const prepareStringAction = (payload: string) => ({
+      payload,
+      meta: { type: 'string_action' }
+    })
+
+    const numberActionMatcher = (a: AnyAction): a is PayloadAction<number> =>
+      a.meta && a.meta.type === 'number_action'
+    const stringActionMatcher = (a: AnyAction): a is PayloadAction<string> =>
+      a.meta && a.meta.type === 'string_action'
+
+    const incrementBy = createAction('increment', prepareNumberAction)
+    const decrementBy = createAction('decrement', prepareNumberAction)
+    const concatWith = createAction('concat', prepareStringAction)
+
+    const initialState = { numberActions: 0, stringActions: 0 }
+    const numberActionsCounter = {
+      matcher: numberActionMatcher,
+      reducer(state: typeof initialState) {
+        state.numberActions = state.numberActions * 10 + 1
+      }
+    }
+    const stringActionsCounter = {
+      matcher: stringActionMatcher,
+      reducer(state: typeof initialState) {
+        state.stringActions = state.stringActions * 10 + 1
+      }
+    }
+
+    test('uses the reducer of matching actionMatchers', () => {
+      const reducer = createReducer(initialState, {}, [
+        numberActionsCounter,
+        stringActionsCounter
+      ])
+      expect(reducer(undefined, incrementBy(1))).toEqual({
+        numberActions: 1,
+        stringActions: 0
+      })
+      expect(reducer(undefined, decrementBy(1))).toEqual({
+        numberActions: 1,
+        stringActions: 0
+      })
+      expect(reducer(undefined, concatWith('foo'))).toEqual({
+        numberActions: 0,
+        stringActions: 1
+      })
+    })
+    test('fallback to default case', () => {
+      const reducer = createReducer(
+        initialState,
+        {},
+        [numberActionsCounter, stringActionsCounter],
+        state => {
+          state.numberActions = -1
+          state.stringActions = -1
+        }
+      )
+      expect(reducer(undefined, { type: 'somethingElse' })).toEqual({
+        numberActions: -1,
+        stringActions: -1
+      })
+    })
+    test('runs reducer cases followed by all matching actionMatchers', () => {
+      const reducer = createReducer(
+        initialState,
+        {
+          [incrementBy.type](state) {
+            state.numberActions = state.numberActions * 10 + 2
+          }
+        },
+        [
+          {
+            matcher: numberActionMatcher,
+            reducer(state) {
+              state.numberActions = state.numberActions * 10 + 3
+            }
+          },
+          numberActionsCounter,
+          stringActionsCounter
+        ]
+      )
+      expect(reducer(undefined, incrementBy(1))).toEqual({
+        numberActions: 231,
+        stringActions: 0
+      })
+      expect(reducer(undefined, decrementBy(1))).toEqual({
+        numberActions: 31,
+        stringActions: 0
+      })
+      expect(reducer(undefined, concatWith('foo'))).toEqual({
+        numberActions: 0,
+        stringActions: 1
+      })
+    })
+    test('works with `actionCreator.match`', () => {
+      const reducer = createReducer(initialState, {}, [
+        {
+          matcher: incrementBy.match,
+          reducer(state) {
+            state.numberActions += 100
+          }
+        }
+      ])
+      expect(reducer(undefined, incrementBy(1))).toEqual({
+        numberActions: 100,
+        stringActions: 0
+      })
+    })
+  })
+
   describe('alternative builder callback for actionMap', () => {
     const increment = createAction<number, 'increment'>('increment')
     const decrement = createAction<number, 'decrement'>('decrement')
@@ -204,6 +318,145 @@ describe('createReducer', () => {
       )
     })
   })
+
+  describe('builder "addMatcher" method', () => {
+    const prepareNumberAction = (payload: number) => ({
+      payload,
+      meta: { type: 'number_action' }
+    })
+    const prepareStringAction = (payload: string) => ({
+      payload,
+      meta: { type: 'string_action' }
+    })
+
+    const numberActionMatcher = (a: AnyAction): a is PayloadAction<number> =>
+      a.meta && a.meta.type === 'number_action'
+    const stringActionMatcher = (a: AnyAction): a is PayloadAction<string> =>
+      a.meta && a.meta.type === 'string_action'
+
+    const incrementBy = createAction('increment', prepareNumberAction)
+    const decrementBy = createAction('decrement', prepareNumberAction)
+    const concatWith = createAction('concat', prepareStringAction)
+
+    const initialState = { numberActions: 0, stringActions: 0 }
+
+    test('uses the reducer of matching actionMatchers', () => {
+      const reducer = createReducer(initialState, builder =>
+        builder
+          .addMatcher(numberActionMatcher, state => {
+            state.numberActions += 1
+          })
+          .addMatcher(stringActionMatcher, state => {
+            state.stringActions += 1
+          })
+      )
+      expect(reducer(undefined, incrementBy(1))).toEqual({
+        numberActions: 1,
+        stringActions: 0
+      })
+      expect(reducer(undefined, decrementBy(1))).toEqual({
+        numberActions: 1,
+        stringActions: 0
+      })
+      expect(reducer(undefined, concatWith('foo'))).toEqual({
+        numberActions: 0,
+        stringActions: 1
+      })
+    })
+    test('falls back to defaultCase', () => {
+      const reducer = createReducer(initialState, builder =>
+        builder
+          .addCase(concatWith, state => {
+            state.stringActions += 1
+          })
+          .addMatcher(numberActionMatcher, state => {
+            state.numberActions += 1
+          })
+          .addDefaultCase(state => {
+            state.numberActions = -1
+            state.stringActions = -1
+          })
+      )
+      expect(reducer(undefined, { type: 'somethingElse' })).toEqual({
+        numberActions: -1,
+        stringActions: -1
+      })
+    })
+    test('runs reducer cases followed by all matching actionMatchers', () => {
+      const reducer = createReducer(initialState, builder =>
+        builder
+          .addCase(incrementBy, state => {
+            state.numberActions = state.numberActions * 10 + 1
+          })
+          .addMatcher(numberActionMatcher, state => {
+            state.numberActions = state.numberActions * 10 + 2
+          })
+          .addMatcher(stringActionMatcher, state => {
+            state.stringActions = state.stringActions * 10 + 1
+          })
+          .addMatcher(numberActionMatcher, state => {
+            state.numberActions = state.numberActions * 10 + 3
+          })
+      )
+      expect(reducer(undefined, incrementBy(1))).toEqual({
+        numberActions: 123,
+        stringActions: 0
+      })
+      expect(reducer(undefined, decrementBy(1))).toEqual({
+        numberActions: 23,
+        stringActions: 0
+      })
+      expect(reducer(undefined, concatWith('foo'))).toEqual({
+        numberActions: 0,
+        stringActions: 1
+      })
+    })
+    test('works with `actionCreator.match`', () => {
+      const reducer = createReducer(initialState, builder =>
+        builder.addMatcher(incrementBy.match, state => {
+          state.numberActions += 100
+        })
+      )
+      expect(reducer(undefined, incrementBy(1))).toEqual({
+        numberActions: 100,
+        stringActions: 0
+      })
+    })
+    test('calling addCase, addMatcher and addDefaultCase in a nonsensical order should result in an error in development mode', () => {
+      expect(() =>
+        createReducer(initialState, (builder: any) =>
+          builder
+            .addMatcher(numberActionMatcher, () => {})
+            .addCase(incrementBy, () => {})
+        )
+      ).toThrowErrorMatchingInlineSnapshot(
+        `"\`builder.addCase\` should only be called before calling \`builder.addMatcher\`"`
+      )
+      expect(() =>
+        createReducer(initialState, (builder: any) =>
+          builder.addDefaultCase(() => {}).addCase(incrementBy, () => {})
+        )
+      ).toThrowErrorMatchingInlineSnapshot(
+        `"\`builder.addCase\` should only be called before calling \`builder.addDefaultCase\`"`
+      )
+      expect(() =>
+        createReducer(initialState, (builder: any) =>
+          builder
+            .addDefaultCase(() => {})
+            .addMatcher(numberActionMatcher, () => {})
+        )
+      ).toThrowErrorMatchingInlineSnapshot(
+        `"\`builder.addMatcher\` should only be called before calling \`builder.addDefaultCase\`"`
+      )
+      expect(() =>
+        createReducer(initialState, (builder: any) =>
+          builder.addDefaultCase(() => {}).addDefaultCase(() => {})
+        )
+      ).toThrowErrorMatchingInlineSnapshot(
+        `"\`builder.addDefaultCase\` can only be called once"`
+      )
+    })
+  })
 })
 
 function behavesLikeReducer(todosReducer: TodosReducer) {
diff --git a/src/createSlice.test.ts b/src/createSlice.test.ts
index b1927854..4ee5fc51 100644
--- a/src/createSlice.test.ts
+++ b/src/createSlice.test.ts
@@ -154,6 +154,32 @@ describe('createSlice', () => {
         )
       })
 
+      test('can be used with addMatcher and type guard functions', () => {
+        const slice = createSlice({
+          name: 'counter',
+          initialState: 0,
+          reducers: {},
+          extraReducers: builder =>
+            builder.addMatcher(
+              increment.match,
+              (state, action: { type: 'increment'; payload: number }) =>
+                state + action.payload
+            )
+        })
+        expect(slice.reducer(0, increment(5))).toBe(5)
+      })
+
+      test('can be used with addDefaultCase', () => {
+        const slice = createSlice({
+          name: 'counter',
+          initialState: 0,
+          reducers: {},
+          extraReducers: builder =>
+            builder.addDefaultCase((state, action) => state + action.payload)
+        })
+        expect(slice.reducer(0, increment(5))).toBe(5)
+      })
+
       // for further tests, see the test of createReducer that goes way more into depth on this
     })
   })
diff --git a/src/entities/unsorted_state_adapter.test.ts b/src/entities/unsorted_state_adapter.test.ts
index 44f5d696..3e4521b5 100644
--- a/src/entities/unsorted_state_adapter.test.ts
+++ b/src/entities/unsorted_state_adapter.test.ts
@@ -4,7 +4,8 @@ import {
   BookModel,
   TheGreatGatsby,
   AClockworkOrange,
-  AnimalFarm
+  AnimalFarm,
+  TheHobbit
 } from './fixtures/book'
 import { createNextState } from '..'
 
@@ -466,12 +467,20 @@ describe('Unsorted State Adapter', () => {
     test('updateMany', () => {
       const firstChange = { title: 'First Change' }
       const secondChange = { title: 'Second Change' }
-      const withMany = adapter.setAll(state, [TheGreatGatsby, AClockworkOrange])
+      const thirdChange = { title: 'Third Change' }
+      const fourthChange = { author: 'Fourth Change' }
+      const withMany = adapter.setAll(state, [
+        TheGreatGatsby,
+        AClockworkOrange,
+        TheHobbit
+      ])
 
       const result = createNextState(withMany, draft => {
         adapter.updateMany(draft, [
-          { id: TheGreatGatsby.id, changes: firstChange },
-          { id: AClockworkOrange.id, changes: secondChange }
+          { id: TheHobbit.id, changes: firstChange },
+          { id: TheGreatGatsby.id, changes: secondChange },
+          { id: AClockworkOrange.id, changes: thirdChange },
+          { id: TheHobbit.id, changes: fourthChange }
         ])
       })
 
@@ -480,16 +489,22 @@ describe('Unsorted State Adapter', () => {
           "entities": Object {
             "aco": Object {
               "id": "aco",
-              "title": "Second Change",
+              "title": "Third Change",
             },
             "tgg": Object {
               "id": "tgg",
+              "title": "Second Change",
+            },
+            "th": Object {
+              "author": "Fourth Change",
+              "id": "th",
               "title": "First Change",
             },
           },
           "ids": Array [
             "tgg",
             "aco",
+            "th",
           ],
         }
       `)
diff --git a/src/serializableStateInvariantMiddleware.test.ts b/src/serializableStateInvariantMiddleware.test.ts
index a512ed50..6e769776 100644
--- a/src/serializableStateInvariantMiddleware.test.ts
+++ b/src/serializableStateInvariantMiddleware.test.ts
@@ -111,7 +111,8 @@ describe('serializableStateInvariantMiddleware', () => {
       Take a look at the logic that dispatched this action:  Object {
         \\"type\\": Symbol(SOME_CONSTANT),
       } 
-      (See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)"
+      (See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants) 
+      (To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)"
     `)
   })
 
@@ -363,7 +364,8 @@ describe('serializableStateInvariantMiddleware', () => {
           },
           \\"type\\": \\"test\\",
         } 
-        (See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)"
+        (See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants) 
+        (To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)"
       `)
     })
 
diff --git a/type-tests/files/mapBuilders.typetest.ts b/type-tests/files/mapBuilders.typetest.ts
index 27a5b91b..a193e358 100644
--- a/type-tests/files/mapBuilders.typetest.ts
+++ b/type-tests/files/mapBuilders.typetest.ts
@@ -1,5 +1,5 @@
 import { executeReducerBuilderCallback } from 'src/mapBuilders'
-import { createAction, CaseReducers } from 'src'
+import { createAction, AnyAction } from 'src'
 
 function expectType<T>(t: T) {
   return t
@@ -50,5 +50,50 @@ function expectType<T>(t: T) {
       'decrement',
       (state, action: ReturnType<typeof increment>) => state
     )
+
+    // action type is inferred
+    builder.addMatcher(increment.match, (state, action) => {
+      expectType<ReturnType<typeof increment>>(action)
+    })
+
+    // addCase().addMatcher() is possible, action type inferred correctly
+    builder
+      .addCase(
+        'increment',
+        (state, action: ReturnType<typeof increment>) => state
+      )
+      .addMatcher(decrement.match, (state, action) => {
+        expectType<ReturnType<typeof decrement>>(action)
+      })
+
+    // addCase().addDefaultCase() is possible, action type is AnyAction
+    builder
+      .addCase(
+        'increment',
+        (state, action: ReturnType<typeof increment>) => state
+      )
+      .addDefaultCase((state, action) => {
+        expectType<AnyAction>(action)
+      })
+
+    {
+      // addMatcher() should prevent further calls to addCase()
+      const b = builder.addMatcher(increment.match, () => {})
+      // typings:expect-error
+      b.addCase(increment, () => {})
+      b.addMatcher(increment.match, () => {})
+      b.addDefaultCase(() => {})
+    }
+
+    {
+      // addDefaultCase() should prevent further calls to addCase(), addMatcher() and addDefaultCase
+      const b = builder.addDefaultCase(() => {})
+      // typings:expect-error
+      b.addCase(increment, () => {})
+      // typings:expect-error
+      b.addMatcher(increment.match, () => {})
+      // typings:expect-error
+      b.addDefaultCase(() => {})
+    }
   })
 }