Repository: reduxjs/redux-toolkit
Pull request #417 (2020-03-10 00:39:59)

Fix bug in docs for createAsyncThunk example
Resolves https://github.com/reduxjs/redux-toolkit/issues/416

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/417/builds/14027) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 9cef7118a9cf2dc14bef6a2f04d516fc89224ad7:

|Sandbox| Source |
|--|--|
|[sharp-wildflower-lpf79](https://codesandbox.io/s/sharp-wildflower-lpf79)| Configuration |
|[cranky-brook-dgs5r](https://codesandbox.io/s/cranky-brook-dgs5r)| Configuration |
|[spring-paper-mgs3j](https://codesandbox.io/s/spring-paper-mgs3j)| Configuration |

markerikson: Hmm.  Now that I look at this, I feel like this needs a bit more thought put into it.

How _would_ you distinguish between "this specific thunk has dispatched the `pending` action and is now doing the payload work", and "some other request is already in flight"?

@msutkowski , @phryneas - any thoughts on this?
dlwalsh: I guess you'd need to store the `requestId` and check against that.
msutkowski: @markerikson With the way `createAsyncThunk` currently works, the user would have to make a simple wrapper that tracked the promise resolution state and use it like: 
`const myTrackedPromise = promiseState(dispatch(myLongRunningAsyncThunk()))`

If this seems like something we'd want to handle internally, a quick and low-effort solution would be to expose a `getStatus` or `isProcessing` function on the promise and have it return the internal state.

```js
const everlongPromise = dispatch(myLongRunningThunk());
// ... do a lot of processing and such?
if (promise.getStatus() === 'processing') {
  // we know we're processing still...
}
```
or

```js
const everlongPromise = dispatch(myLongRunningThunk());
// ... do a lot of processing and such?
if (promise.isProcessing()) {
  // we know we're processing still...
}
```

Would someone actually want more of the internals like `requestId`, or just to know if we're still in the middle of processing?
msutkowski: Here is a basic sample of that idea: https://github.com/reduxjs/redux-toolkit/pull/418. Switched the naming from my example above to match the standardized naming.
phryneas: I also think we should just go with the `requestId`. 
Also, I don't know if the `payloadCreator` itself should generally check that it should be running. It might make sense in this case, but that is a very edgy case and I wouldn't really use that as an example then.
Generally: It has been started and not been cancelled, so it runs. I would assume that it's only the job of the reducer to ignore (or not) actions sent from the thunk.

So this would be my attempt:

```ts
import { createSlice, createAsyncThunk, SerializedError } from "@reduxjs/toolkit";

function fetchById(id: string) {
  return new Promise<{ data: string }>(resolve => setTimeout(resolve, 1000, { data: "test" }));
}

const fetchUserById = createAsyncThunk("users/fetchByIdStatus", async (userId: string) => {
  if (getState().slice.requestId !== requestId){
    // if you really do want to do something like this, I'd do it this way
    return
  }
  const response = await fetchById(userId);
  return response.data;
});

const usersSlice = createSlice({
  name: "users",
  initialState: {
    currentRequestId: undefined as undefined|string,
    entities: [] as string[],
    loading: "idle",
    error: null as null | SerializedError
  },
  reducers: {},
  extraReducers: builder =>
    builder
      .addCase(fetchUserById.pending, (state, action) => {
        if (state.loading === "idle") {
          state.loading = "pending";
          state.currentRequestId = action.meta.requestId
        }
      })
      .addCase(fetchUserById.fulfilled, (state, action) => {
        if (state.currentRequestId === action.meta.requestId) {
          state.loading = "idle";
          state.entities.push(action.payload);
        }
      })
      .addCase(fetchUserById.rejected, (state, action) => {
        if (state.currentRequestId === action.meta.requestId) {
          state.loading = "idle";
          state.error = action.error;
        }
      })
});
```
dlwalsh: Updated PR to implement the pattern above.
dlwalsh: Thanks for the feedback @phryneas. I've updated the code snippet accordingly.

You've also clarified my own confusion around the `createAsyncThunk` API. I suspect the fact that the fulfilled action still fires on early exit will be surprising to some. It was to me.
phryneas: > Thanks for the feedback @phryneas. I've updated the code snippet accordingly.
> 
> You've also clarified my own confusion around the `createAsyncThunk` API. I suspect the fact that the fulfilled action still fires on early exit will be surprising to some. It was to me.

A return statement is a return statement and will trigger a `fulfilled` action. 
Maybe `throw`ing something in that example might be better - then it'd be a `rejected` action. 
But something will definitely happen - it also does when cancelling the thunk by calling `.abort()` on it - it's always guaranteed to have a `pending` action followed by either an `fulfilled` or `rejected` action.

Patch:
diff --git a/docs/api/createAsyncThunk.md b/docs/api/createAsyncThunk.md
index 3110795c..9e6c7a2c 100644
--- a/docs/api/createAsyncThunk.md
+++ b/docs/api/createAsyncThunk.md
@@ -325,9 +325,9 @@ import { userAPI } from './userAPI'
 
 const fetchUserById = createAsyncThunk(
   'users/fetchByIdStatus',
-  async (userId, { getState }) => {
-    const { loading } = getState().users
-    if (loading !== 'idle') {
+  async (userId, { getState, requestId }) => {
+    const { currentRequestId, loading } = getState().users
+    if (loading !== 'pending' || requestId !== currentRequestId) {
       return
     }
     const response = await userAPI.fetchById(userId)
@@ -340,6 +340,7 @@ const usersSlice = createSlice({
   initialState: {
     entities: [],
     loading: 'idle',
+    currentRequestId: undefined,
     error: null
   },
   reducers: {},
@@ -347,18 +348,23 @@ const usersSlice = createSlice({
     [fetchUserById.pending]: (state, action) => {
       if (state.loading === 'idle') {
         state.loading = 'pending'
+        state.currentRequestId = action.meta.requestId
       }
     },
     [fetchUserById.fulfilled]: (state, action) => {
-      if (state.loading === 'pending') {
+      const { requestId } = action.meta
+      if (state.loading === 'pending' && state.currentRequestId === requestId) {
         state.loading = 'idle'
-        state.push(action.payload)
+        state.entities.push(action.payload)
+        state.currentRequestId = undefined
       }
     },
     [fetchUserById.rejected]: (state, action) => {
-      if (state.loading === 'pending') {
+      const { requestId } = action.meta
+      if (state.loading === 'pending' && state.currentRequestId === requestId) {
         state.loading = 'idle'
         state.error = action.error
+        state.currentRequestId = undefined
       }
     }
   }
diff --git a/docs/api/createEntityAdapter.md b/docs/api/createEntityAdapter.md
index d3ffda2e..373b8ef2 100644
--- a/docs/api/createEntityAdapter.md
+++ b/docs/api/createEntityAdapter.md
@@ -118,8 +118,6 @@ export interface Dictionary<T> extends DictionaryNum<T> {
 
 export type Update<T> = { id: EntityId; changes: Partial<T> }
 
-export type EntityMap<T> = (entity: T) => T
-
 export interface EntityState<T> {
   ids: EntityId[]
   entities: Dictionary<T>
@@ -171,9 +169,6 @@ export interface EntityStateAdapter<T> {
     state: S,
     entities: PayloadAction<T[]>
   ): S
-
-  map<S extends EntityState<T>>(state: S, map: EntityMap<T>): S
-  map<S extends EntityState<T>>(state: S, map: PayloadAction<EntityMap<T>>): S
 }
 
 export interface EntitySelectors<T, V> {
@@ -208,7 +203,6 @@ The primary content of an entity adapter is a set of generated reducer functions
 - `updateMany`: accepts an array of update objects, and updates all corresponding entities
 - `upsertOne`: accepts a single entity. If an entity with that ID exists, the fields in the update will be merged into the existing entity, with any matching fields overwriting the existing values. If the entity does not exist, it will be added.
 - `upsertMany`: accepts an array of entities that will be upserted.
-- `map`: accepts a callback function that will be run against each existing entity, and may return a change description object. Afterwards, all changes will be merged into the corresponding existing entities.
 
 Each method has a signature that looks like:
 
diff --git a/etc/redux-toolkit.api.md b/etc/redux-toolkit.api.md
index 8b19b879..870e272c 100644
--- a/etc/redux-toolkit.api.md
+++ b/etc/redux-toolkit.api.md
@@ -28,8 +28,7 @@ export interface ActionCreatorWithNonInferrablePayload<T extends string = string
 
 // @public
 export interface ActionCreatorWithOptionalPayload<P, T extends string = string> extends BaseActionCreator<P, T> {
-    (payload?: undefined): PayloadAction<undefined, T>;
-    <PT extends Diff<P, undefined>>(payload?: PT): PayloadAction<PT, T>;
+    (payload?: P): PayloadAction<P, T>;
 }
 
 // @public
@@ -39,7 +38,6 @@ export interface ActionCreatorWithoutPayload<T extends string = string> extends
 
 // @public
 export interface ActionCreatorWithPayload<P, T extends string = string> extends BaseActionCreator<P, T> {
-    <PT extends P>(payload: PT): PayloadAction<PT, T>;
     (payload: P): PayloadAction<P, T>;
 }
 
@@ -187,9 +185,6 @@ export interface EntityAdapter<T> extends EntityStateAdapter<T> {
     sortComparer: false | Comparer<T>;
 }
 
-// @alpha (undocumented)
-export type EntityMap<T> = (entity: T) => T;
-
 // @alpha (undocumented)
 export interface EntityState<T> {
     // (undocumented)
@@ -251,6 +246,7 @@ export interface SerializableStateInvariantMiddlewareOptions {
     ignoredActions?: string[];
     ignoredPaths?: string[];
     isSerializable?: (value: any) => boolean;
+    warnAfter?: number;
 }
 
 // @alpha (undocumented)
diff --git a/src/createAction.ts b/src/createAction.ts
index 2dea13b3..93558d7b 100644
--- a/src/createAction.ts
+++ b/src/createAction.ts
@@ -123,16 +123,12 @@ export interface ActionCreatorWithPreparedPayload<
  */
 export interface ActionCreatorWithOptionalPayload<P, T extends string = string>
   extends BaseActionCreator<P, T> {
-  /**
-   * Calling this {@link redux#ActionCreator} without arguments will
-   * return a {@link PayloadAction} of type `T` with a payload of `undefined`
-   */
-  (payload?: undefined): PayloadAction<undefined, T>
   /**
    * Calling this {@link redux#ActionCreator} with an argument will
-   * return a {@link PayloadAction} of type `T` with a payload of `P`
+   * return a {@link PayloadAction} of type `T` with a payload of `P`.
+   * Calling it without an argument will return a PayloadAction with a payload of `undefined`.
    */
-  <PT extends Diff<P, undefined>>(payload?: PT): PayloadAction<PT, T>
+  (payload?: P): PayloadAction<P, T>
 }
 
 /**
@@ -160,12 +156,6 @@ export interface ActionCreatorWithoutPayload<T extends string = string>
  */
 export interface ActionCreatorWithPayload<P, T extends string = string>
   extends BaseActionCreator<P, T> {
-  /**
-   * Calling this {@link redux#ActionCreator} with an argument will
-   * return a {@link PayloadAction} of type `T` with a payload of `P`
-   * If possible, `P` will be narrowed down to the exact type of the payload argument.
-   */
-  <PT extends P>(payload: PT): PayloadAction<PT, T>
   /**
    * Calling this {@link redux#ActionCreator} with an argument will
    * return a {@link PayloadAction} of type `T` with a payload of `P`
@@ -333,8 +323,6 @@ export function getType<T extends string>(
 
 // helper types for more readable typings
 
-type Diff<T, U> = T extends U ? never : T
-
 type IfPrepareActionMethodProvided<
   PA extends PrepareAction<any> | void,
   True,
diff --git a/src/entities/index.ts b/src/entities/index.ts
index eded42c9..e6fe1d87 100644
--- a/src/entities/index.ts
+++ b/src/entities/index.ts
@@ -4,7 +4,6 @@ export {
   EntityState,
   EntityAdapter,
   Update,
-  EntityMap,
   IdSelector,
   Comparer
 } from './models'
diff --git a/src/entities/models.ts b/src/entities/models.ts
index 9f7b217d..30996a57 100644
--- a/src/entities/models.ts
+++ b/src/entities/models.ts
@@ -1,4 +1,5 @@
 import { PayloadAction } from '../createAction'
+import { IsAny } from '../tsHelpers'
 
 /**
  * @alpha
@@ -34,11 +35,6 @@ export interface Dictionary<T> extends DictionaryNum<T> {
  */
 export type Update<T> = { id: EntityId; changes: Partial<T> }
 
-/**
- * @alpha
- */
-export type EntityMap<T> = (entity: T) => T
-
 /**
  * @alpha
  */
@@ -52,50 +48,76 @@ export interface EntityDefinition<T> {
   sortComparer: false | Comparer<T>
 }
 
+export type PreventAny<S, T> = IsAny<S, EntityState<T>, S>
+
 export interface EntityStateAdapter<T> {
-  addOne<S extends EntityState<T>>(state: S, entity: T): S
-  addOne<S extends EntityState<T>>(state: S, action: PayloadAction<T>): S
+  addOne<S extends EntityState<T>>(state: PreventAny<S, T>, entity: T): S
+  addOne<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    action: PayloadAction<T>
+  ): S
 
-  addMany<S extends EntityState<T>>(state: S, entities: T[]): S
-  addMany<S extends EntityState<T>>(state: S, entities: PayloadAction<T[]>): S
+  addMany<S extends EntityState<T>>(state: PreventAny<S, T>, entities: T[]): S
+  addMany<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    entities: PayloadAction<T[]>
+  ): S
 
-  setAll<S extends EntityState<T>>(state: S, entities: T[]): S
-  setAll<S extends EntityState<T>>(state: S, entities: PayloadAction<T[]>): S
+  setAll<S extends EntityState<T>>(state: PreventAny<S, T>, entities: T[]): S
+  setAll<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    entities: PayloadAction<T[]>
+  ): S
 
-  removeOne<S extends EntityState<T>>(state: S, key: EntityId): S
-  removeOne<S extends EntityState<T>>(state: S, key: PayloadAction<EntityId>): S
+  removeOne<S extends EntityState<T>>(state: PreventAny<S, T>, key: EntityId): S
+  removeOne<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    key: PayloadAction<EntityId>
+  ): S
 
-  removeMany<S extends EntityState<T>>(state: S, keys: EntityId[]): S
   removeMany<S extends EntityState<T>>(
-    state: S,
+    state: PreventAny<S, T>,
+    keys: EntityId[]
+  ): S
+  removeMany<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
     keys: PayloadAction<EntityId[]>
   ): S
 
-  removeAll<S extends EntityState<T>>(state: S): S
+  removeAll<S extends EntityState<T>>(state: PreventAny<S, T>): S
 
-  updateOne<S extends EntityState<T>>(state: S, update: Update<T>): S
   updateOne<S extends EntityState<T>>(
-    state: S,
+    state: PreventAny<S, T>,
+    update: Update<T>
+  ): S
+  updateOne<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
     update: PayloadAction<Update<T>>
   ): S
 
-  updateMany<S extends EntityState<T>>(state: S, updates: Update<T>[]): S
   updateMany<S extends EntityState<T>>(
-    state: S,
+    state: PreventAny<S, T>,
+    updates: Update<T>[]
+  ): S
+  updateMany<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
     updates: PayloadAction<Update<T>[]>
   ): S
 
-  upsertOne<S extends EntityState<T>>(state: S, entity: T): S
-  upsertOne<S extends EntityState<T>>(state: S, entity: PayloadAction<T>): S
+  upsertOne<S extends EntityState<T>>(state: PreventAny<S, T>, entity: T): S
+  upsertOne<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    entity: PayloadAction<T>
+  ): S
 
-  upsertMany<S extends EntityState<T>>(state: S, entities: T[]): S
   upsertMany<S extends EntityState<T>>(
-    state: S,
+    state: PreventAny<S, T>,
+    entities: T[]
+  ): S
+  upsertMany<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
     entities: PayloadAction<T[]>
   ): S
-
-  map<S extends EntityState<T>>(state: S, map: EntityMap<T>): S
-  map<S extends EntityState<T>>(state: S, map: PayloadAction<EntityMap<T>>): S
 }
 
 export interface EntitySelectors<T, V> {
diff --git a/src/entities/sorted_state_adapter.ts b/src/entities/sorted_state_adapter.ts
index 79e6e03f..93c6e5e7 100644
--- a/src/entities/sorted_state_adapter.ts
+++ b/src/entities/sorted_state_adapter.ts
@@ -3,9 +3,7 @@ import {
   IdSelector,
   Comparer,
   EntityStateAdapter,
-  Update,
-  EntityMap,
-  EntityId
+  Update
 } from './models'
 import { createStateOperator } from './state_adapter'
 import { createUnsortedStateAdapter } from './unsorted_state_adapter'
@@ -72,21 +70,6 @@ export function createSortedStateAdapter<T>(
     }
   }
 
-  function mapMutably(updatesOrMap: EntityMap<T>, state: R): void {
-    const updates: Update<T>[] = state.ids.reduce(
-      (changes: Update<T>[], id: EntityId) => {
-        const change = updatesOrMap(state.entities[id]!)
-        if (change !== state.entities[id]) {
-          changes.push({ id, changes: change })
-        }
-        return changes
-      },
-      []
-    )
-
-    updateManyMutably(updates, state)
-  }
-
   function upsertOneMutably(entity: T, state: R): void {
     return upsertManyMutably([entity], state)
   }
@@ -151,7 +134,6 @@ export function createSortedStateAdapter<T>(
     setAll: createStateOperator(setAllMutably),
     addMany: createStateOperator(addManyMutably),
     updateMany: createStateOperator(updateManyMutably),
-    upsertMany: createStateOperator(upsertManyMutably),
-    map: createStateOperator(mapMutably)
+    upsertMany: createStateOperator(upsertManyMutably)
   }
 }
diff --git a/src/entities/unsorted_state_adapter.ts b/src/entities/unsorted_state_adapter.ts
index 5791abea..7386d9a8 100644
--- a/src/entities/unsorted_state_adapter.ts
+++ b/src/entities/unsorted_state_adapter.ts
@@ -3,7 +3,6 @@ import {
   EntityStateAdapter,
   IdSelector,
   Update,
-  EntityMap,
   EntityId
 } from './models'
 import { createStateOperator } from './state_adapter'
@@ -57,7 +56,7 @@ export function createUnsortedStateAdapter<T>(
     }
   }
 
-  function removeAll<S extends R>(state: S): S {
+  function removeAll(state: R): any {
     return Object.assign({}, state, {
       ids: [],
       entities: {}
@@ -120,22 +119,6 @@ export function createUnsortedStateAdapter<T>(
     }
   }
 
-  function mapMutably(map: EntityMap<T>, state: R): void {
-    const changes: Update<T>[] = state.ids.reduce(
-      (changes: Update<T>[], id: EntityId) => {
-        const change = map(state.entities[id]!)
-        if (change !== state.entities[id]) {
-          changes.push({ id, changes: change })
-        }
-        return changes
-      },
-      []
-    )
-    const updates = changes.filter(({ id }) => id in state.entities)
-
-    return updateManyMutably(updates, state)
-  }
-
   function upsertOneMutably(entity: T, state: R): void {
     return upsertManyMutably([entity], state)
   }
@@ -167,7 +150,6 @@ export function createUnsortedStateAdapter<T>(
     upsertOne: createStateOperator(upsertOneMutably),
     upsertMany: createStateOperator(upsertManyMutably),
     removeOne: createStateOperator(removeOneMutably),
-    removeMany: createStateOperator(removeManyMutably),
-    map: createStateOperator(mapMutably)
+    removeMany: createStateOperator(removeManyMutably)
   }
 }
diff --git a/src/immutableStateInvariantMiddleware.ts b/src/immutableStateInvariantMiddleware.ts
index 22bab404..b80f3731 100644
--- a/src/immutableStateInvariantMiddleware.ts
+++ b/src/immutableStateInvariantMiddleware.ts
@@ -1,4 +1,5 @@
 import { Middleware } from 'redux'
+import { getTimeMeasureUtils } from './utils'
 
 type EntryProcessor = (key: string, value: any) => any
 
@@ -174,6 +175,7 @@ type IsImmutableFunc = (value: any) => boolean
 export interface ImmutableStateInvariantMiddlewareOptions {
   isImmutable?: IsImmutableFunc
   ignoredPaths?: string[]
+  warnAfter?: number
 }
 
 export function createImmutableStateInvariantMiddleware(
@@ -183,7 +185,11 @@ export function createImmutableStateInvariantMiddleware(
     return () => next => action => next(action)
   }
 
-  const { isImmutable = isImmutableDefault, ignoredPaths } = options
+  const {
+    isImmutable = isImmutableDefault,
+    ignoredPaths,
+    warnAfter = 32
+  } = options
   const track = trackForMutations.bind(null, isImmutable, ignoredPaths)
 
   return ({ getState }) => {
@@ -192,39 +198,51 @@ export function createImmutableStateInvariantMiddleware(
 
     let result
     return next => action => {
-      state = getState()
-
-      result = tracker.detectMutations()
-      // Track before potentially not meeting the invariant
-      tracker = track(state)
-
-      invariant(
-        !result.wasMutated,
-        `A state mutation was detected between dispatches, in the path '${(
-          result.path || []
-        ).join(
-          '.'
-        )}'.  This may cause incorrect behavior. (http://redux.js.org/docs/Troubleshooting.html#never-mutate-reducer-arguments)`
+      const measureUtils = getTimeMeasureUtils(
+        warnAfter,
+        'ImmutableStateInvariantMiddleware'
       )
 
-      const dispatchedAction = next(action)
-      state = getState()
+      measureUtils.measureTime(() => {
+        state = getState()
 
-      result = tracker.detectMutations()
-      // Track before potentially not meeting the invariant
-      tracker = track(state)
+        result = tracker.detectMutations()
+        // Track before potentially not meeting the invariant
+        tracker = track(state)
 
-      result.wasMutated &&
         invariant(
           !result.wasMutated,
-          `A state mutation was detected inside a dispatch, in the path: ${(
+          `A state mutation was detected between dispatches, in the path '${(
             result.path || []
           ).join(
             '.'
-          )}. Take a look at the reducer(s) handling the action ${stringify(
-            action
-          )}. (http://redux.js.org/docs/Troubleshooting.html#never-mutate-reducer-arguments)`
+          )}'.  This may cause incorrect behavior. (http://redux.js.org/docs/Troubleshooting.html#never-mutate-reducer-arguments)`
         )
+      })
+
+      const dispatchedAction = next(action)
+
+      measureUtils.measureTime(() => {
+        state = getState()
+
+        result = tracker.detectMutations()
+        // Track before potentially not meeting the invariant
+        tracker = track(state)
+
+        result.wasMutated &&
+          invariant(
+            !result.wasMutated,
+            `A state mutation was detected inside a dispatch, in the path: ${(
+              result.path || []
+            ).join(
+              '.'
+            )}. Take a look at the reducer(s) handling the action ${stringify(
+              action
+            )}. (http://redux.js.org/docs/Troubleshooting.html#never-mutate-reducer-arguments)`
+          )
+      })
+
+      measureUtils.warnIfExceeded()
 
       return dispatchedAction
     }
diff --git a/src/index.ts b/src/index.ts
index aa484c2f..c61b9095 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -75,7 +75,6 @@ export {
   EntityState,
   EntityAdapter,
   Update,
-  EntityMap,
   IdSelector,
   Comparer
 } from './entities/models'
diff --git a/src/serializableStateInvariantMiddleware.ts b/src/serializableStateInvariantMiddleware.ts
index 978b0a7a..a4955425 100644
--- a/src/serializableStateInvariantMiddleware.ts
+++ b/src/serializableStateInvariantMiddleware.ts
@@ -1,5 +1,6 @@
 import isPlainObject from './isPlainObject'
 import { Middleware } from 'redux'
+import { getTimeMeasureUtils } from './utils'
 
 /**
  * Returns true if the passed value is "plain", i.e. a value that is either
@@ -114,6 +115,10 @@ export interface SerializableStateInvariantMiddlewareOptions {
    * An array of dot-separated path strings to ignore when checking for serializability, Defaults to []
    */
   ignoredPaths?: string[]
+  /**
+   * Execution time warning threshold. If the middleware takes longer than `warnAfter` ms, a warning will be displayed in the console. Defaults to 32
+   */
+  warnAfter?: number
 }
 
 /**
@@ -135,7 +140,8 @@ export function createSerializableStateInvariantMiddleware(
     isSerializable = isPlain,
     getEntries,
     ignoredActions = [],
-    ignoredPaths = []
+    ignoredPaths = [],
+    warnAfter = 32
   } = options
 
   return storeAPI => next => action => {
@@ -143,48 +149,58 @@ export function createSerializableStateInvariantMiddleware(
       return next(action)
     }
 
-    const foundActionNonSerializableValue = findNonSerializableValue(
-      action,
-      [],
-      isSerializable,
-      getEntries
+    const measureUtils = getTimeMeasureUtils(
+      warnAfter,
+      'SerializableStateInvariantMiddleware'
     )
-
-    if (foundActionNonSerializableValue) {
-      const { keyPath, value } = foundActionNonSerializableValue
-
-      console.error(
-        `A non-serializable value was detected in an action, in the path: \`${keyPath}\`. Value:`,
-        value,
-        '\nTake a look at the logic that dispatched this action: ',
+    measureUtils.measureTime(() => {
+      const foundActionNonSerializableValue = findNonSerializableValue(
         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)'
+        [],
+        isSerializable,
+        getEntries
       )
-    }
+
+      if (foundActionNonSerializableValue) {
+        const { keyPath, value } = foundActionNonSerializableValue
+
+        console.error(
+          `A non-serializable value was detected in an action, in the path: \`${keyPath}\`. Value:`,
+          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)'
+        )
+      }
+    })
 
     const result = next(action)
 
-    const state = storeAPI.getState()
+    measureUtils.measureTime(() => {
+      const state = storeAPI.getState()
 
-    const foundStateNonSerializableValue = findNonSerializableValue(
-      state,
-      [],
-      isSerializable,
-      getEntries,
-      ignoredPaths
-    )
+      const foundStateNonSerializableValue = findNonSerializableValue(
+        state,
+        [],
+        isSerializable,
+        getEntries,
+        ignoredPaths
+      )
 
-    if (foundStateNonSerializableValue) {
-      const { keyPath, value } = foundStateNonSerializableValue
+      if (foundStateNonSerializableValue) {
+        const { keyPath, value } = foundStateNonSerializableValue
 
-      console.error(
-        `A non-serializable value was detected in the state, in the path: \`${keyPath}\`. Value:`,
-        value,
-        `
+        console.error(
+          `A non-serializable value was detected in the state, in the path: \`${keyPath}\`. Value:`,
+          value,
+          `
 Take a look at the reducer(s) handling this action type: ${action.type}.
 (See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)`
-      )
-    }
+        )
+      }
+    })
+
+    measureUtils.warnIfExceeded()
 
     return result
   }
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 00000000..933c7f4a
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,21 @@
+export function getTimeMeasureUtils(maxDelay: number, fnName: string) {
+  let elapsed = 0
+  return {
+    measureTime<T>(fn: () => T): T {
+      const started = Date.now()
+      try {
+        return fn()
+      } finally {
+        const finished = Date.now()
+        elapsed += finished - started
+      }
+    },
+    warnIfExceeded() {
+      if (elapsed > maxDelay) {
+        console.warn(`${fnName} took ${elapsed}ms, which is more than the warning threshold of ${maxDelay}ms. 
+If your state or actions are very large, you may want to disable the middleware as it might cause too much of a slowdown in development mode. See https://redux-toolkit.js.org/api/getDefaultMiddleware for instructions.
+It is disabled in production builds, so you don't need to worry about that.`)
+      }
+    }
+  }
+}


Test patch:
diff --git a/src/entities/sorted_state_adapter.test.ts b/src/entities/sorted_state_adapter.test.ts
index 13419d78..8b45a788 100644
--- a/src/entities/sorted_state_adapter.test.ts
+++ b/src/entities/sorted_state_adapter.test.ts
@@ -331,40 +331,6 @@ describe('Sorted State Adapter', () => {
     })
   })
 
-  it('should let you map over entities in the state', () => {
-    const firstChange = { ...TheGreatGatsby, title: 'First change' }
-    const secondChange = { ...AClockworkOrange, title: 'Second change' }
-
-    const withMany = adapter.setAll(state, [
-      TheGreatGatsby,
-      AClockworkOrange,
-      AnimalFarm
-    ])
-
-    const withUpdates = adapter.map(withMany, book =>
-      book.title === TheGreatGatsby.title
-        ? firstChange
-        : book.title === AClockworkOrange.title
-        ? secondChange
-        : book
-    )
-
-    expect(withUpdates).toEqual({
-      ids: [AnimalFarm.id, TheGreatGatsby.id, AClockworkOrange.id],
-      entities: {
-        [AnimalFarm.id]: AnimalFarm,
-        [TheGreatGatsby.id]: {
-          ...TheGreatGatsby,
-          ...firstChange
-        },
-        [AClockworkOrange.id]: {
-          ...AClockworkOrange,
-          ...secondChange
-        }
-      }
-    })
-  })
-
   it('should let you add one entity to the state with upsert()', () => {
     const withOneEntity = adapter.upsertOne(state, TheGreatGatsby)
     expect(withOneEntity).toEqual({
diff --git a/src/entities/unsorted_state_adapter.test.ts b/src/entities/unsorted_state_adapter.test.ts
index 5b83f145..c4448337 100644
--- a/src/entities/unsorted_state_adapter.test.ts
+++ b/src/entities/unsorted_state_adapter.test.ts
@@ -250,40 +250,6 @@ describe('Unsorted State Adapter', () => {
     expect(entities.c).toBeTruthy()
   })
 
-  it('should let you map over entities in the state', () => {
-    const firstChange = { ...TheGreatGatsby, title: 'First change' }
-    const secondChange = { ...AClockworkOrange, title: 'Second change' }
-
-    const withMany = adapter.setAll(state, [
-      TheGreatGatsby,
-      AClockworkOrange,
-      AnimalFarm
-    ])
-
-    const withUpdates = adapter.map(withMany, book =>
-      book.title === TheGreatGatsby.title
-        ? firstChange
-        : book.title === AClockworkOrange.title
-        ? secondChange
-        : book
-    )
-
-    expect(withUpdates).toEqual({
-      ids: [TheGreatGatsby.id, AClockworkOrange.id, AnimalFarm.id],
-      entities: {
-        [TheGreatGatsby.id]: {
-          ...TheGreatGatsby,
-          ...firstChange
-        },
-        [AClockworkOrange.id]: {
-          ...AClockworkOrange,
-          ...secondChange
-        },
-        [AnimalFarm.id]: AnimalFarm
-      }
-    })
-  })
-
   it('should let you add one entity to the state with upsert()', () => {
     const withOneEntity = adapter.upsertOne(state, TheGreatGatsby)
     expect(withOneEntity).toEqual({
diff --git a/src/immutableStateInvariantMiddleware.test.ts b/src/immutableStateInvariantMiddleware.test.ts
index 38763d65..104e677d 100644
--- a/src/immutableStateInvariantMiddleware.test.ts
+++ b/src/immutableStateInvariantMiddleware.test.ts
@@ -5,6 +5,7 @@ import {
   trackForMutations,
   ImmutableStateInvariantMiddlewareOptions
 } from './immutableStateInvariantMiddleware'
+import { mockConsole, createConsole, getLog } from 'console-testing-library'
 
 describe('createImmutableStateInvariantMiddleware', () => {
   let state: { foo: { bar: number[]; baz: string } }
@@ -121,6 +122,42 @@ describe('createImmutableStateInvariantMiddleware', () => {
       dispatch({ type: 'SOME_ACTION' })
     }).not.toThrow()
   })
+
+  it('Should print a warning if execution takes too long', () => {
+    state.foo.bar = new Array(10000).fill({ value: 'more' })
+
+    const next: Dispatch = action => action
+
+    const dispatch = middleware({ warnAfter: 4 })(next)
+
+    const restore = mockConsole(createConsole())
+    try {
+      dispatch({ type: 'SOME_ACTION' })
+      expect(getLog().log).toMatch(
+        /^ImmutableStateInvariantMiddleware took \d*ms, which is more than the warning threshold of 4ms./
+      )
+    } finally {
+      restore()
+    }
+  })
+
+  it('Should not print a warning if "next" takes too long', () => {
+    const next: Dispatch = action => {
+      const started = Date.now()
+      while (Date.now() - started < 8) {}
+      return action
+    }
+
+    const dispatch = middleware({ warnAfter: 4 })(next)
+
+    const restore = mockConsole(createConsole())
+    try {
+      dispatch({ type: 'SOME_ACTION' })
+      expect(getLog().log).toEqual('')
+    } finally {
+      restore()
+    }
+  })
 })
 
 describe('trackForMutations', () => {
diff --git a/src/serializableStateInvariantMiddleware.test.ts b/src/serializableStateInvariantMiddleware.test.ts
index 9c3476f8..c435b659 100644
--- a/src/serializableStateInvariantMiddleware.test.ts
+++ b/src/serializableStateInvariantMiddleware.test.ts
@@ -408,4 +408,51 @@ describe('serializableStateInvariantMiddleware', () => {
     expect(log).toBe('')
     const q = 42
   })
+
+  it('Should print a warning if execution takes too long', () => {
+    const reducer: Reducer = (state = 42, action) => {
+      return state
+    }
+
+    const serializableStateInvariantMiddleware = createSerializableStateInvariantMiddleware(
+      { warnAfter: 4 }
+    )
+
+    const store = configureStore({
+      reducer: {
+        testSlice: reducer
+      },
+      middleware: [serializableStateInvariantMiddleware]
+    })
+
+    store.dispatch({
+      type: 'SOME_ACTION',
+      payload: new Array(10000).fill({ value: 'more' })
+    })
+    expect(getLog().log).toMatch(
+      /^SerializableStateInvariantMiddleware took \d*ms, which is more than the warning threshold of 4ms./
+    )
+  })
+
+  it('Should not print a warning if "reducer" takes too long', () => {
+    const reducer: Reducer = (state = 42, action) => {
+      const started = Date.now()
+      while (Date.now() - started < 8) {}
+      return state
+    }
+
+    const serializableStateInvariantMiddleware = createSerializableStateInvariantMiddleware(
+      { warnAfter: 4 }
+    )
+
+    const store = configureStore({
+      reducer: {
+        testSlice: reducer
+      },
+      middleware: [serializableStateInvariantMiddleware]
+    })
+
+    store.dispatch({ type: 'SOME_ACTION' })
+    expect(getLog().log).toMatch('')
+  })
 })
diff --git a/type-tests/files/createAction.typetest.ts b/type-tests/files/createAction.typetest.ts
index 702913a6..e702070b 100644
--- a/type-tests/files/createAction.typetest.ts
+++ b/type-tests/files/createAction.typetest.ts
@@ -73,9 +73,9 @@ function expectType<T>(p: T): T {
     { type: 'action' }
   ) as PayloadActionCreator<number | undefined>
 
-  expectType<PayloadAction<number>>(actionCreator(1))
-  expectType<PayloadAction<undefined>>(actionCreator())
-  expectType<PayloadAction<undefined>>(actionCreator(undefined))
+  expectType<PayloadAction<number | undefined>>(actionCreator(1))
+  expectType<PayloadAction<number | undefined>>(actionCreator())
+  expectType<PayloadAction<number | undefined>>(actionCreator(undefined))
 
   // typings:expect-error
   expectType<PayloadAction<number>>(actionCreator())
@@ -104,9 +104,9 @@ function expectType<T>(p: T): T {
     { type: 'action' }
   ) as PayloadActionCreator<number>
 
-  const actionCreator2: ActionCreator<
-    PayloadAction<number>
-  > = payloadActionCreator2
+  const actionCreator2: ActionCreator<PayloadAction<
+    number
+  >> = payloadActionCreator2
 }
 
 /* createAction() */
diff --git a/type-tests/files/createEntityAdapter.typetest.ts b/type-tests/files/createEntityAdapter.typetest.ts
new file mode 100644
index 00000000..c53a29ff
--- /dev/null
+++ b/type-tests/files/createEntityAdapter.typetest.ts
@@ -0,0 +1,113 @@
+import {
+  createSlice,
+  createEntityAdapter,
+  EntityAdapter,
+  ActionCreatorWithPayload,
+  ActionCreatorWithoutPayload
+} from 'src'
+import { EntityStateAdapter, EntityId, Update } from 'src/entities/models'
+
+function expectType<T>(t: T) {
+  return t
+}
+
+function extractReducers<T>(
+  adapter: EntityAdapter<T>
+): Omit<EntityStateAdapter<T>, 'map'> {
+  const {
+    selectId,
+    sortComparer,
+    getInitialState,
+    getSelectors,
+    ...rest
+  } = adapter
+  return rest
+}
+
+/**
+ * should be usable in a slice, with all the "reducer-like" functions
+ */
+{
+  type Entity = {
+    value: string
+  }
+  const adapter = createEntityAdapter<Entity>()
+  const slice = createSlice({
+    name: 'test',
+    initialState: adapter.getInitialState(),
+    reducers: {
+      ...extractReducers(adapter)
+    }
+  })
+
+  expectType<ActionCreatorWithPayload<Entity>>(slice.actions.addOne)
+  expectType<ActionCreatorWithPayload<Entity[]>>(slice.actions.addMany)
+  expectType<ActionCreatorWithPayload<Entity[]>>(slice.actions.setAll)
+  expectType<ActionCreatorWithPayload<EntityId>>(slice.actions.removeOne)
+  expectType<ActionCreatorWithPayload<EntityId[]>>(slice.actions.removeMany)
+  expectType<ActionCreatorWithoutPayload>(slice.actions.removeAll)
+  expectType<ActionCreatorWithPayload<Update<Entity>>>(slice.actions.updateOne)
+  expectType<ActionCreatorWithPayload<Update<Entity>[]>>(
+    slice.actions.updateMany
+  )
+  expectType<ActionCreatorWithPayload<Entity>>(slice.actions.upsertOne)
+  expectType<ActionCreatorWithPayload<Entity[]>>(slice.actions.upsertMany)
+}
+
+/**
+ * should not be able to mix with a different EntityAdapter
+ */
+{
+  type Entity = {
+    value: string
+  }
+  type Entity2 = {
+    value2: string
+  }
+  const adapter = createEntityAdapter<Entity>()
+  const adapter2 = createEntityAdapter<Entity2>()
+  createSlice({
+    name: 'test',
+    initialState: adapter.getInitialState(),
+    reducers: {
+      addOne: adapter.addOne,
+      // typings:expect-error
+      addOne2: adapter2.addOne
+    }
+  })
+}
+
+/**
+ * should be usable in a slice with extra properties
+ */
+{
+  type Entity = {
+    value: string
+  }
+  const adapter = createEntityAdapter<Entity>()
+  createSlice({
+    name: 'test',
+    initialState: adapter.getInitialState({ extraData: 'test' }),
+    reducers: {
+      addOne: adapter.addOne
+    }
+  })
+}
+
+/**
+ * should not be usable in a slice with an unfitting state
+ */
+{
+  type Entity = {
+    value: string
+  }
+  const adapter = createEntityAdapter<Entity>()
+  createSlice({
+    name: 'test',
+    initialState: { somethingElse: '' },
+    reducers: {
+      // typings:expect-error
+      addOne: adapter.addOne
+    }
+  })
+}