Repository: reduxjs/redux-toolkit

Problem statement:
[RTK v2.0] createApi (React) exceeds the maximum length the compiler will serialize
Version: `2.0.0-beta.0`
Hello, I'm having an issue inferring types when using `createApi` from `@reduxjs/toolkit/query/react`.

I get this issue with a very minimal setup.

[Code Sandbox link](https://codesandbox.io/p/sandbox/rtk-type-portability-j99lyh?file=%2Fsrc%2Frtk%2FmyApiReact.ts%3A6%2C18)
```typescript
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

/**
 * `myApiReact` defaults to the `any` type producing the TypeScript error....
 * The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed.ts(7056)
 */ 
export const myApiReact = createApi({
  reducerPath: "myApiReact",
  baseQuery: fetchBaseQuery({
    baseUrl: "/",
  }),
  endpoints: (builder) => ({
    getData: builder.query<{}, string>({
      query: (id) => `data/${id}`,
    }),
  }),
});
```

I can't find the cause of it yet. Seems like there's some type recursion happening somewhere in `enhanceEndpoints` when the react module is used? Maybe someone has a better clue.


Hints:

Patch:
diff --git a/docs/virtual/redux-logger/index.ts b/docs/virtual/redux-logger/index.ts
new file mode 100644
--- /dev/null
+++ b/docs/virtual/redux-logger/index.ts
@@ -0,0 +1,7 @@
+import type { Middleware } from 'redux'
+
+declare const logger: Middleware
+
+export { logger }
+
+export default logger
diff --git a/examples/action-listener/counter/src/services/counter/listeners.ts b/examples/action-listener/counter/src/services/counter/listeners.ts
--- a/examples/action-listener/counter/src/services/counter/listeners.ts
+++ b/examples/action-listener/counter/src/services/counter/listeners.ts
@@ -1,6 +1,6 @@
 import { counterActions, counterSelectors } from './slice'
 import {
-  AnyAction,
+  UnknownAction,
   isAllOf,
   isAnyOf,
   PayloadAction,
@@ -11,7 +11,7 @@ import type { AppListenerEffectAPI, AppStartListening } from '../../store'
 function shouldStopAsyncTasksOf(id: string) {
   return isAllOf(
     isAnyOf(counterActions.cancelAsyncUpdates, counterActions.removeCounter),
-    (action: AnyAction): action is PayloadAction<string> =>
+    (action: UnknownAction): action is PayloadAction<string> =>
       action?.payload === id
   )
 }
diff --git a/examples/publish-ci/cra4/public/mockServiceWorker.js b/examples/publish-ci/cra4/public/mockServiceWorker.js
--- a/examples/publish-ci/cra4/public/mockServiceWorker.js
+++ b/examples/publish-ci/cra4/public/mockServiceWorker.js
@@ -2,7 +2,7 @@
 /* tslint:disable */
 
 /**
- * Mock Service Worker (0.49.3).
+ * Mock Service Worker (1.3.2).
  * @see https://github.com/mswjs/msw
  * - Please do NOT modify this file.
  * - Please do NOT serve this file on production.
diff --git a/examples/publish-ci/cra4/src/mocks/handlers.ts b/examples/publish-ci/cra4/src/mocks/handlers.ts
--- a/examples/publish-ci/cra4/src/mocks/handlers.ts
+++ b/examples/publish-ci/cra4/src/mocks/handlers.ts
@@ -34,7 +34,8 @@ export const handlers = [
   }),
 
   rest.get('/posts/:id', (req, res, ctx) => {
-    const { id } = req.params as { id: string }
+    const { id: idParam } = req.params as { id: string }
+    const id = parseInt(idParam, 10)
     state = adapter.updateOne(state, {
       id,
       changes: { fetched_at: new Date().toUTCString() },
diff --git a/examples/publish-ci/cra5/public/mockServiceWorker.js b/examples/publish-ci/cra5/public/mockServiceWorker.js
--- a/examples/publish-ci/cra5/public/mockServiceWorker.js
+++ b/examples/publish-ci/cra5/public/mockServiceWorker.js
@@ -2,7 +2,7 @@
 /* tslint:disable */
 
 /**
- * Mock Service Worker (0.49.3).
+ * Mock Service Worker (1.3.2).
  * @see https://github.com/mswjs/msw
  * - Please do NOT modify this file.
  * - Please do NOT serve this file on production.
diff --git a/examples/publish-ci/cra5/src/mocks/handlers.ts b/examples/publish-ci/cra5/src/mocks/handlers.ts
--- a/examples/publish-ci/cra5/src/mocks/handlers.ts
+++ b/examples/publish-ci/cra5/src/mocks/handlers.ts
@@ -34,7 +34,8 @@ export const handlers = [
   }),
 
   rest.get('/posts/:id', (req, res, ctx) => {
-    const { id } = req.params as { id: string }
+    const { id: idParam } = req.params as { id: string }
+    const id = parseInt(idParam, 10)
     state = adapter.updateOne(state, {
       id,
       changes: { fetched_at: new Date().toUTCString() },
diff --git a/examples/publish-ci/next/public/mockServiceWorker.js b/examples/publish-ci/next/public/mockServiceWorker.js
--- a/examples/publish-ci/next/public/mockServiceWorker.js
+++ b/examples/publish-ci/next/public/mockServiceWorker.js
@@ -2,7 +2,7 @@
 /* tslint:disable */
 
 /**
- * Mock Service Worker (0.49.3).
+ * Mock Service Worker (1.3.2).
  * @see https://github.com/mswjs/msw
  * - Please do NOT modify this file.
  * - Please do NOT serve this file on production.
diff --git a/examples/publish-ci/next/src/mocks/handlers.ts b/examples/publish-ci/next/src/mocks/handlers.ts
--- a/examples/publish-ci/next/src/mocks/handlers.ts
+++ b/examples/publish-ci/next/src/mocks/handlers.ts
@@ -34,7 +34,8 @@ export const handlers = [
   }),
 
   rest.get('/posts/:id', (req, res, ctx) => {
-    const { id } = req.params as { id: string }
+    const { id: idParam } = req.params as { id: string }
+    const id = parseInt(idParam, 10)
     state = adapter.updateOne(state, {
       id,
       changes: { fetched_at: new Date().toUTCString() },
diff --git a/examples/publish-ci/node-esm/test-cjs.cjs b/examples/publish-ci/node-esm/test-cjs.cjs
--- a/examples/publish-ci/node-esm/test-cjs.cjs
+++ b/examples/publish-ci/node-esm/test-cjs.cjs
@@ -30,9 +30,9 @@ for (let [fn, name, category] of entries) {
 }
 
 const moduleNames = [
-  ['@reduxjs/toolkit', 'dist/index.js'],
-  ['@reduxjs/toolkit/query', 'dist/query/index.js'],
-  ['@reduxjs/toolkit/query/react', 'dist/query/react/index.js'],
+  ['@reduxjs/toolkit', 'dist/cjs/index.js'],
+  ['@reduxjs/toolkit/query', 'dist/query/cjs/index.js'],
+  ['@reduxjs/toolkit/query/react', 'dist/query/react/cjs/index.js'],
 ]
 
 for (let [moduleName, expectedFilename] of moduleNames) {
diff --git a/examples/publish-ci/node-esm/test-esm.mjs b/examples/publish-ci/node-esm/test-esm.mjs
--- a/examples/publish-ci/node-esm/test-esm.mjs
+++ b/examples/publish-ci/node-esm/test-esm.mjs
@@ -33,9 +33,12 @@ for (let [fn, name, category] of entries) {
 }
 
 const moduleNames = [
-  ['@reduxjs/toolkit', 'dist/index.js'],
-  ['@reduxjs/toolkit/query', 'dist/query/index.js'],
-  ['@reduxjs/toolkit/query/react', 'dist/query/react/index.js'],
+  ['@reduxjs/toolkit', 'dist/redux-toolkit.modern.mjs'],
+  ['@reduxjs/toolkit/query', 'dist/query/rtk-query.modern.mjs'],
+  [
+    '@reduxjs/toolkit/query/react',
+    'dist/query/react/rtk-query-react.modern.mjs',
+  ],
 ]
 
 ;(async () => {
diff --git a/examples/publish-ci/node-standard/test-cjs.js b/examples/publish-ci/node-standard/test-cjs.js
--- a/examples/publish-ci/node-standard/test-cjs.js
+++ b/examples/publish-ci/node-standard/test-cjs.js
@@ -30,9 +30,9 @@ for (let [fn, name, category] of entries) {
 }
 
 const moduleNames = [
-  ['@reduxjs/toolkit', 'dist/index.js'],
-  ['@reduxjs/toolkit/query', 'dist/query/index.js'],
-  ['@reduxjs/toolkit/query/react', 'dist/query/react/index.js'],
+  ['@reduxjs/toolkit', 'dist/cjs/index.js'],
+  ['@reduxjs/toolkit/query', 'dist/query/cjs/index.js'],
+  ['@reduxjs/toolkit/query/react', 'dist/query/react/cjs/index.js'],
 ]
 
 for (let [moduleName, expectedFilename] of moduleNames) {
diff --git a/examples/publish-ci/node-standard/test-esm.mjs b/examples/publish-ci/node-standard/test-esm.mjs
--- a/examples/publish-ci/node-standard/test-esm.mjs
+++ b/examples/publish-ci/node-standard/test-esm.mjs
@@ -33,9 +33,12 @@ for (let [fn, name, category] of entries) {
 }
 
 const moduleNames = [
-  ['@reduxjs/toolkit', 'dist/index.js'],
-  ['@reduxjs/toolkit/query', 'dist/query/index.js'],
-  ['@reduxjs/toolkit/query/react', 'dist/query/react/index.js'],
+  ['@reduxjs/toolkit', 'dist/redux-toolkit.modern.mjs'],
+  ['@reduxjs/toolkit/query', 'dist/query/rtk-query.modern.mjs'],
+  [
+    '@reduxjs/toolkit/query/react',
+    'dist/query/react/rtk-query-react.modern.mjs',
+  ],
 ]
 
 ;(async () => {
diff --git a/examples/publish-ci/vite/public/mockServiceWorker.js b/examples/publish-ci/vite/public/mockServiceWorker.js
--- a/examples/publish-ci/vite/public/mockServiceWorker.js
+++ b/examples/publish-ci/vite/public/mockServiceWorker.js
@@ -2,7 +2,7 @@
 /* tslint:disable */
 
 /**
- * Mock Service Worker (0.49.3).
+ * Mock Service Worker (1.3.2).
  * @see https://github.com/mswjs/msw
  * - Please do NOT modify this file.
  * - Please do NOT serve this file on production.
diff --git a/examples/publish-ci/vite/src/mocks/handlers.ts b/examples/publish-ci/vite/src/mocks/handlers.ts
--- a/examples/publish-ci/vite/src/mocks/handlers.ts
+++ b/examples/publish-ci/vite/src/mocks/handlers.ts
@@ -34,7 +34,8 @@ export const handlers = [
   }),
 
   rest.get('/posts/:id', (req, res, ctx) => {
-    const { id } = req.params as { id: string }
+    const { id: idParam } = req.params as { id: string }
+    const id = parseInt(idParam, 10)
     state = adapter.updateOne(state, {
       id,
       changes: { fetched_at: new Date().toUTCString() },
diff --git a/examples/query/react/kitchen-sink/src/mocks/handlers.ts b/examples/query/react/kitchen-sink/src/mocks/handlers.ts
--- a/examples/query/react/kitchen-sink/src/mocks/handlers.ts
+++ b/examples/query/react/kitchen-sink/src/mocks/handlers.ts
@@ -69,13 +69,15 @@ export const handlers = [
   }),
 
   rest.get('/posts/:id', (req, res, ctx) => {
-    const { id } = req.params as { id: string };
+    const { id: idParam } = req.params as { id: string }
+    const id = parseInt(idParam, 10)
     state = adapter.updateOne(state, { id, changes: { fetched_at: new Date().toUTCString() } });
     return res(ctx.json(state.entities[id]), ctx.delay(400));
   }),
 
   rest.put('/posts/:id', (req, res, ctx) => {
-    const { id } = req.params as { id: string };
+    const { id: idParam } = req.params as { id: string }
+    const id = parseInt(idParam, 10)
     const changes = req.body as Partial<Post>;
 
     state = adapter.updateOne(state, { id, changes });
@@ -84,7 +86,8 @@ export const handlers = [
   }),
 
   rest.delete('/posts/:id', (req, res, ctx) => {
-    const { id } = req.params as { id: string };
+    const { id: idParam } = req.params as { id: string }
+    const id = parseInt(idParam, 10)
 
     state = adapter.removeOne(state, id);
 
diff --git a/packages/rtk-codemods/.eslintrc.js b/packages/rtk-codemods/.eslintrc.js
--- a/packages/rtk-codemods/.eslintrc.js
+++ b/packages/rtk-codemods/.eslintrc.js
@@ -4,7 +4,7 @@ module.exports = {
   },
 
   plugins: ['prettier', 'node'],
-  extends: ['eslint:recommended', 'plugin:prettier/recommended', 'plugin:node/recommended'],
+  extends: ['eslint:recommended', 'plugin:prettier/recommended'],
   env: {
     node: true,
   },
diff --git a/packages/rtk-codemods/jest.config.js b/packages/rtk-codemods/jest.config.js
deleted file mode 100644
--- a/packages/rtk-codemods/jest.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = {
-  testEnvironment: 'node',
-  automock: false,
-  // roots: ['v2.0/__tests__'],
-  transform: { '\\.ts$': ['ts-jest'] },
-};
diff --git a/packages/rtk-codemods/transformTestUtils.ts b/packages/rtk-codemods/transformTestUtils.ts
new file mode 100644
--- /dev/null
+++ b/packages/rtk-codemods/transformTestUtils.ts
@@ -0,0 +1,61 @@
+import { describe, vi } from 'vitest';
+import type { Transform } from 'jscodeshift';
+import globby from 'globby';
+import fs from 'fs-extra';
+import path from 'path';
+import { runInlineTest } from 'jscodeshift/dist/testUtils';
+
+export function runTransformTest(
+  name: string,
+  transform: Transform,
+  parser: string,
+  fixturePath: string
+) {
+  describe(name, function () {
+    globby
+      .sync('**/*.input.*', {
+        cwd: fixturePath,
+        absolute: true,
+      })
+      .map((entry) => entry.slice(fixturePath.length))
+      .forEach((filename) => {
+        let extension = path.extname(filename);
+        let testName = filename.replace(`.input${extension}`, '');
+        let testInputPath = path.join(fixturePath, `${testName}${extension}`);
+        let inputPath = path.join(fixturePath, `${testName}.input${extension}`);
+        let outputPath = path.join(fixturePath, `${testName}.output${extension}`);
+        let optionsPath = path.join(fixturePath, `${testName}.options.json`);
+        let options = fs.pathExistsSync(optionsPath) ? fs.readFileSync(optionsPath) : '{}';
+
+        describe(testName, function () {
+          beforeEach(function () {
+            process.env.CODEMOD_CLI_ARGS = options;
+          });
+
+          afterEach(function () {
+            process.env.CODEMOD_CLI_ARGS = '';
+          });
+
+          it('transforms correctly', function () {
+            runInlineTest(
+              transform,
+              {},
+              { path: testInputPath, source: fs.readFileSync(inputPath, 'utf8') },
+              fs.readFileSync(outputPath, 'utf8'),
+              { parser }
+            );
+          });
+
+          it('is idempotent', function () {
+            runInlineTest(
+              transform,
+              {},
+              { path: testInputPath, source: fs.readFileSync(outputPath, 'utf8') },
+              fs.readFileSync(outputPath, 'utf8'),
+              { parser }
+            );
+          });
+        });
+      });
+  });
+}
diff --git a/packages/rtk-codemods/transforms/createSliceReducerBuilder/__testfixtures__/basic-ts.input.ts b/packages/rtk-codemods/transforms/createSliceReducerBuilder/__testfixtures__/basic-ts.input.ts
new file mode 100644
--- /dev/null
+++ b/packages/rtk-codemods/transforms/createSliceReducerBuilder/__testfixtures__/basic-ts.input.ts
@@ -0,0 +1,27 @@
+const aSlice = createSlice({
+  name: 'name',
+  initialState: todoAdapter.getInitialState(),
+  reducers: {
+    property: () => {},
+    method(state, action: PayloadAction<Todo>) {
+      todoAdapter.addOne(state, action);
+    },
+    identifier: todoAdapter.removeOne,
+    preparedProperty: {
+      prepare: (todo: Todo) => ({ payload: { id: nanoid(), ...todo } }),
+      reducer: () => {}
+    },
+    preparedMethod: {
+      prepare(todo: Todo) {
+        return { payload: { id: nanoid(), ...todo } }
+      },
+      reducer(state, action: PayloadAction<Todo>) {
+        todoAdapter.addOne(state, action);
+      }
+    },
+    preparedIdentifier: {
+      prepare: withPayload(),
+      reducer: todoAdapter.setMany
+    },
+  }
+})
\ No newline at end of file
diff --git a/packages/rtk-codemods/transforms/createSliceReducerBuilder/__testfixtures__/basic-ts.output.ts b/packages/rtk-codemods/transforms/createSliceReducerBuilder/__testfixtures__/basic-ts.output.ts
new file mode 100644
--- /dev/null
+++ b/packages/rtk-codemods/transforms/createSliceReducerBuilder/__testfixtures__/basic-ts.output.ts
@@ -0,0 +1,23 @@
+const aSlice = createSlice({
+  name: 'name',
+  initialState: todoAdapter.getInitialState(),
+
+  reducers: (create) => ({
+    property: create.reducer(() => {}),
+
+    method: create.reducer((state, action: PayloadAction<Todo>) => {
+      todoAdapter.addOne(state, action);
+    }),
+
+    identifier: create.reducer(todoAdapter.removeOne),
+    preparedProperty: create.preparedReducer((todo: Todo) => ({ payload: { id: nanoid(), ...todo } }), () => {}),
+
+    preparedMethod: create.preparedReducer((todo: Todo) => {
+      return { payload: { id: nanoid(), ...todo } }
+    }, (state, action: PayloadAction<Todo>) => {
+      todoAdapter.addOne(state, action);
+    }),
+
+    preparedIdentifier: create.preparedReducer(withPayload(), todoAdapter.setMany)
+  })
+})
\ No newline at end of file
diff --git a/packages/rtk-codemods/transforms/createSliceReducerBuilder/__testfixtures__/basic.input.js b/packages/rtk-codemods/transforms/createSliceReducerBuilder/__testfixtures__/basic.input.js
new file mode 100644
--- /dev/null
+++ b/packages/rtk-codemods/transforms/createSliceReducerBuilder/__testfixtures__/basic.input.js
@@ -0,0 +1,27 @@
+const aSlice = createSlice({
+  name: 'name',
+  initialState: todoAdapter.getInitialState(),
+  reducers: {
+    property: () => {},
+    method(state, action) {
+      todoAdapter.setMany(state, action);
+    },
+    identifier: todoAdapter.removeOne,
+    preparedProperty: {
+      prepare: (todo) => ({ payload: { id: nanoid(), ...todo } }),
+      reducer: () => {}
+    },
+    preparedMethod: {
+      prepare(todo) {
+        return { payload: { id: nanoid(), ...todo } }
+      },
+      reducer(state, action) {
+        todoAdapter.setMany(state, action);
+      }
+    },
+    preparedIdentifier: {
+      prepare: withPayload(),
+      reducer: todoAdapter.setMany
+    },
+  }
+})
\ No newline at end of file
diff --git a/packages/rtk-codemods/transforms/createSliceReducerBuilder/__testfixtures__/basic.output.js b/packages/rtk-codemods/transforms/createSliceReducerBuilder/__testfixtures__/basic.output.js
new file mode 100644
--- /dev/null
+++ b/packages/rtk-codemods/transforms/createSliceReducerBuilder/__testfixtures__/basic.output.js
@@ -0,0 +1,23 @@
+const aSlice = createSlice({
+  name: 'name',
+  initialState: todoAdapter.getInitialState(),
+
+  reducers: (create) => ({
+    property: create.reducer(() => {}),
+
+    method: create.reducer((state, action) => {
+      todoAdapter.setMany(state, action);
+    }),
+
+    identifier: create.reducer(todoAdapter.removeOne),
+    preparedProperty: create.preparedReducer((todo) => ({ payload: { id: nanoid(), ...todo } }), () => {}),
+
+    preparedMethod: create.preparedReducer((todo) => {
+      return { payload: { id: nanoid(), ...todo } }
+    }, (state, action) => {
+      todoAdapter.setMany(state, action);
+    }),
+
+    preparedIdentifier: create.preparedReducer(withPayload(), todoAdapter.setMany)
+  })
+})
\ No newline at end of file
diff --git a/packages/rtk-codemods/transforms/createSliceReducerBuilder/index.ts b/packages/rtk-codemods/transforms/createSliceReducerBuilder/index.ts
new file mode 100644
--- /dev/null
+++ b/packages/rtk-codemods/transforms/createSliceReducerBuilder/index.ts
@@ -0,0 +1,179 @@
+/* eslint-disable node/no-extraneous-import */
+/* eslint-disable node/no-unsupported-features/es-syntax */
+import type { ExpressionKind, SpreadElementKind } from 'ast-types/gen/kinds';
+import type {
+  CallExpression,
+  JSCodeshift,
+  ObjectExpression,
+  ObjectProperty,
+  Transform,
+} from 'jscodeshift';
+
+function creatorCall(j: JSCodeshift, type: 'reducer', reducer: ExpressionKind): CallExpression;
+// eslint-disable-next-line no-redeclare
+function creatorCall(
+  j: JSCodeshift,
+  type: 'preparedReducer',
+  prepare: ExpressionKind,
+  reducer: ExpressionKind
+): CallExpression;
+// eslint-disable-next-line no-redeclare
+function creatorCall(
+  j: JSCodeshift,
+  type: 'reducer' | 'preparedReducer',
+  ...rest: Array<ExpressionKind | SpreadElementKind>
+) {
+  return j.callExpression(j.memberExpression(j.identifier('create'), j.identifier(type)), rest);
+}
+
+export function reducerPropsToBuilderExpression(j: JSCodeshift, defNode: ObjectExpression) {
+  const returnedObject = j.objectExpression([]);
+  for (let property of defNode.properties) {
+    let finalProp: ObjectProperty | undefined;
+    switch (property.type) {
+      case 'ObjectMethod': {
+        const { key, params, body } = property;
+        finalProp = j.objectProperty(
+          key,
+          creatorCall(j, 'reducer', j.arrowFunctionExpression(params, body))
+        );
+        break;
+      }
+      case 'ObjectProperty': {
+        const { key } = property;
+
+        switch (property.value.type) {
+          case 'ObjectExpression': {
+            let preparedReducerParams: { prepare?: ExpressionKind; reducer?: ExpressionKind } = {};
+
+            for (const objProp of property.value.properties) {
+              switch (objProp.type) {
+                case 'ObjectMethod': {
+                  const { key, params, body } = objProp;
+                  if (
+                    key.type === 'Identifier' &&
+                    (key.name === 'reducer' || key.name === 'prepare')
+                  ) {
+                    preparedReducerParams[key.name] = j.arrowFunctionExpression(params, body);
+                  }
+                  break;
+                }
+                case 'ObjectProperty': {
+                  const { key, value } = objProp;
+
+                  let finalExpression: ExpressionKind | undefined = undefined;
+
+                  switch (value.type) {
+                    case 'ArrowFunctionExpression':
+                    case 'FunctionExpression':
+                    case 'Identifier':
+                    case 'MemberExpression':
+                    case 'CallExpression': {
+                      finalExpression = value;
+                    }
+                  }
+
+                  if (
+                    key.type === 'Identifier' &&
+                    (key.name === 'reducer' || key.name === 'prepare') &&
+                    finalExpression
+                  ) {
+                    preparedReducerParams[key.name] = finalExpression;
+                  }
+                  break;
+                }
+              }
+            }
+
+            if (preparedReducerParams.prepare && preparedReducerParams.reducer) {
+              finalProp = j.objectProperty(
+                key,
+                creatorCall(
+                  j,
+                  'preparedReducer',
+                  preparedReducerParams.prepare,
+                  preparedReducerParams.reducer
+                )
+              );
+            } else if (preparedReducerParams.reducer) {
+              finalProp = j.objectProperty(
+                key,
+                creatorCall(j, 'reducer', preparedReducerParams.reducer)
+              );
+            }
+            break;
+          }
+          case 'ArrowFunctionExpression':
+          case 'FunctionExpression':
+          case 'Identifier':
+          case 'MemberExpression':
+          case 'CallExpression': {
+            const { value } = property;
+            finalProp = j.objectProperty(key, creatorCall(j, 'reducer', value));
+            break;
+          }
+        }
+        break;
+      }
+    }
+    if (!finalProp) {
+      continue;
+    }
+    returnedObject.properties.push(finalProp);
+  }
+
+  return j.arrowFunctionExpression([j.identifier('create')], returnedObject, true);
+}
+
+const transform: Transform = (file, api) => {
+  const j = api.jscodeshift;
+
+  return (
+    j(file.source)
+      // @ts-ignore some expression mismatch
+      .find(j.CallExpression, {
+        callee: { name: 'createSlice' },
+        // @ts-ignore some expression mismatch
+        arguments: { 0: { type: 'ObjectExpression' } },
+      })
+
+      .filter((path) => {
+        const createSliceArgsObject = path.node.arguments[0] as ObjectExpression;
+        return createSliceArgsObject.properties.some(
+          (p) =>
+            p.type === 'ObjectProperty' &&
+            p.key.type === 'Identifier' &&
+            p.key.name === 'reducers' &&
+            p.value.type === 'ObjectExpression'
+        );
+      })
+      .forEach((path) => {
+        const createSliceArgsObject = path.node.arguments[0] as ObjectExpression;
+        j(path).replaceWith(
+          j.callExpression(j.identifier('createSlice'), [
+            j.objectExpression(
+              createSliceArgsObject.properties.map((p) => {
+                if (
+                  p.type === 'ObjectProperty' &&
+                  p.key.type === 'Identifier' &&
+                  p.key.name === 'reducers' &&
+                  p.value.type === 'ObjectExpression'
+                ) {
+                  const expressionStatement = reducerPropsToBuilderExpression(j, p.value);
+                  return j.objectProperty(p.key, expressionStatement);
+                }
+                return p;
+              })
+            ),
+          ])
+        );
+      })
+      .toSource({
+        arrowParensAlways: true,
+      })
+  );
+};
+
+export const parser = 'tsx';
+
+export default transform;
diff --git a/packages/rtk-codemods/vitest.config.ts b/packages/rtk-codemods/vitest.config.ts
new file mode 100644
--- /dev/null
+++ b/packages/rtk-codemods/vitest.config.ts
@@ -0,0 +1,13 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  test: {
+    globals: true,
+    setupFiles: [],
+    include: ['./transforms/**/*.(spec|test).[jt]s?(x)'],
+    alias: {},
+    deps: {
+      interopDefault: true,
+    },
+  },
+});
diff --git a/packages/rtk-query-codegen-openapi/jest.config.js b/packages/rtk-query-codegen-openapi/jest.config.js
--- a/packages/rtk-query-codegen-openapi/jest.config.js
+++ b/packages/rtk-query-codegen-openapi/jest.config.js
@@ -1,12 +1,13 @@
 const { resolve } = require('path');
 
-const tsConfigPath = resolve('./test/tsconfig');
+const tsConfigPath = resolve('./test/tsconfig.json');
 
 /** @typedef {import('ts-jest/dist/types')} */
 /** @type {import('@jest/types').Config.InitialOptions} */
 const config = {
   rootDir: './test',
   setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
+  preset: 'ts-jest',
   globals: {
     'ts-jest': {
       tsconfig: tsConfigPath,
diff --git a/packages/rtk-query-graphql-request-base-query/src/index.ts b/packages/rtk-query-graphql-request-base-query/src/index.ts
--- a/packages/rtk-query-graphql-request-base-query/src/index.ts
+++ b/packages/rtk-query-graphql-request-base-query/src/index.ts
@@ -1,7 +1,7 @@
 import { isPlainObject } from '@reduxjs/toolkit'
 import type { BaseQueryFn } from '@reduxjs/toolkit/query'
 import type { DocumentNode } from 'graphql'
-import { GraphQLClient, ClientError } from 'graphql-request'
+import { GraphQLClient, ClientError, RequestOptions } from 'graphql-request'
 import type {
   ErrorResponse,
   GraphqlRequestBaseQueryArgs,
@@ -43,7 +43,7 @@ export const graphqlRequestBaseQuery = <E = ErrorResponse>(
         data: await client.request({
           document,
           variables,
-          signal,
+          signal: signal as unknown as RequestOptions['signal'],
           requestHeaders: preparedHeaders,
         }),
         meta: {},
@@ -52,10 +52,10 @@ export const graphqlRequestBaseQuery = <E = ErrorResponse>(
       if (error instanceof ClientError) {
         const { name, message, stack, request, response } = error
 
-      const customErrors =
-        options.customErrors ?? (() => ({ name, message, stack }));
+        const customErrors =
+          options.customErrors ?? (() => ({ name, message, stack }))
 
-      const customizedErrors = customErrors(error) as E;
+        const customizedErrors = customErrors(error) as E
 
         return { error: customizedErrors, meta: { request, response } }
       }
diff --git a/packages/toolkit/.size-limit.js b/packages/toolkit/.size-limit.js
--- a/packages/toolkit/.size-limit.js
+++ b/packages/toolkit/.size-limit.js
@@ -19,30 +19,30 @@ function withRtkPath(suffix) {
         join(__dirname)
       ),
       new webpack.NormalModuleReplacementPlugin(
-        /rtk-query-react.esm.js/,
+        /rtk-query-react.modern.js/,
         (r) => {
           const old = r.request
           r.request = r.request.replace(
-            /rtk-query-react.esm.js$/,
+            /rtk-query-react.modern.js$/,
             `rtk-query-react.${suffix}`
           )
           // console.log(old, '=>', r.request)
         }
       ),
-      new webpack.NormalModuleReplacementPlugin(/rtk-query.esm.js/, (r) => {
+      new webpack.NormalModuleReplacementPlugin(/rtk-query.modern.js/, (r) => {
         const old = r.request
         r.request = r.request.replace(
-          /rtk-query.esm.js$/,
+          /rtk-query.modern.js$/,
           `rtk-query.${suffix}`
         )
         // console.log(old, '=>', r.request)
       }),
       new webpack.NormalModuleReplacementPlugin(
-        /redux-toolkit.esm.js$/,
+        /redux-toolkit.modern.js$/,
         (r) => {
           const old = r.request
           r.request = r.request.replace(
-            /redux-toolkit.esm.js$/,
+            /redux-toolkit.modern.js$/,
             `redux-toolkit.${suffix}`
           )
           // console.log(old, '=>', r.request)
@@ -69,29 +69,29 @@ const ignoreAll = [
 module.exports = [
   {
     name: `1. entry point: @reduxjs/toolkit`,
-    path: 'dist/redux-toolkit.esm.js',
+    path: 'dist/redux-toolkit.modern.js',
   },
   {
     name: `1. entry point: @reduxjs/toolkit/query`,
-    path: 'dist/query/rtk-query.esm.js',
+    path: 'dist/query/rtk-query.modern.js',
   },
   {
     name: `1. entry point: @reduxjs/toolkit/query/react`,
-    path: 'dist/query/react/rtk-query-react.esm.js',
+    path: 'dist/query/react/rtk-query-react.modern.js',
   },
   {
     name: `2. entry point: @reduxjs/toolkit (without dependencies)`,
-    path: 'dist/redux-toolkit.esm.js',
+    path: 'dist/redux-toolkit.modern.js',
     ignore: ignoreAll,
   },
   {
     name: `2. entry point: @reduxjs/toolkit/query (without dependencies)`,
-    path: 'dist/query/rtk-query.esm.js',
+    path: 'dist/query/rtk-query.modern.js',
     ignore: ignoreAll,
   },
   {
     name: `2. entry point: @reduxjs/toolkit/query/react (without dependencies)`,
-    path: 'dist/query/react/rtk-query-react.esm.js',
+    path: 'dist/query/react/rtk-query-react.modern.js',
     ignore: ignoreAll,
   },
 ]
diff --git a/packages/toolkit/jest.config.js b/packages/toolkit/jest.config.js
deleted file mode 100644
--- a/packages/toolkit/jest.config.js
+++ /dev/null
@@ -1,22 +0,0 @@
-module.exports = {
-  testEnvironment: 'jest-environment-jsdom',
-  setupFilesAfterEnv: ['./jest.setup.js'],
-  testMatch: ['<rootDir>/src/**/*.(spec|test).[jt]s?(x)'],
-  moduleNameMapper: {
-    '^@reduxjs/toolkit$': '<rootDir>/src/index.ts', // @remap-prod-remove-line
-    '^@reduxjs/toolkit/query$': '<rootDir>/src/query/index.ts', // @remap-prod-remove-line
-    '^@reduxjs/toolkit/query/react$': '<rootDir>/src/query/react/index.ts', // @remap-prod-remove-line
-    // this mapping is disabled as we want `dist` imports in the tests only to be used for "type-only" imports which don't play a role for jest
-    //'^@reduxjs/toolkit/dist/(.*)$': '<rootDir>/src/*',
-    '^@internal/(.*)$': '<rootDir>/src/$1',
-  },
-  preset: 'ts-jest',
-  globals: {
-    'ts-jest': {
-      tsconfig: 'tsconfig.test.json',
-      diagnostics: {
-        ignoreCodes: [6133],
-      },
-    },
-  },
-}
diff --git a/packages/toolkit/scripts/build.ts b/packages/toolkit/scripts/build.ts
deleted file mode 100644
--- a/packages/toolkit/scripts/build.ts
+++ /dev/null
@@ -1,365 +0,0 @@
-/* eslint-disable import/first */
-// @ts-check
-import { build } from 'esbuild'
-import terser from 'terser'
-import rollup from 'rollup'
-import path from 'path'
-import fs from 'fs-extra'
-import ts from 'typescript'
-import type { RawSourceMap } from 'source-map'
-import merge from 'merge-source-map'
-import type { ExtractorResult } from '@microsoft/api-extractor'
-import { Extractor, ExtractorConfig } from '@microsoft/api-extractor'
-import yargs from 'yargs/yargs'
-
-import { extractInlineSourcemap, removeInlineSourceMap } from './sourcemap'
-import type { BuildOptions, EntryPointOptions } from './types'
-import { appendInlineSourceMap, getLocation } from './sourcemap'
-
-const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
-
-const { argv } = yargs(process.argv)
-  .option('local', {
-    alias: 'l',
-    type: 'boolean',
-    description: 'Run API Extractor in local mode',
-  })
-  .option('skipExtraction', {
-    alias: 's',
-    type: 'boolean',
-    description: 'Skip running API extractor',
-  })
-
-const outputDir = path.join(__dirname, '../dist')
-
-const buildTargets: BuildOptions[] = [
-  {
-    format: 'cjs',
-    name: 'cjs.development',
-    minify: false,
-    env: 'development',
-  },
-
-  {
-    format: 'cjs',
-    name: 'cjs.production.min',
-    minify: true,
-    env: 'production',
-  },
-  // ESM, embedded `process`, ES5 syntax: typical Webpack dev
-  {
-    format: 'esm',
-    name: 'esm',
-    minify: false,
-    env: '',
-  },
-  // ESM, embedded `process`, ES2017 syntax: modern Webpack dev
-  {
-    format: 'esm',
-    name: 'modern',
-    target: 'es2017',
-    minify: false,
-    env: '',
-  },
-  // ESM, pre-compiled "dev", ES2017 syntax: browser development
-  {
-    format: 'esm',
-    name: 'modern.development',
-    target: 'es2017',
-    minify: false,
-    env: 'development',
-  },
-  // ESM, pre-compiled "prod", ES2017 syntax: browser prod
-  {
-    format: 'esm',
-    name: 'modern.production.min',
-    target: 'es2017',
-    minify: true,
-    env: 'production',
-  },
-  {
-    format: 'umd',
-    name: 'umd',
-    minify: false,
-    env: 'development',
-  },
-  {
-    format: 'umd',
-    name: 'umd.min',
-    minify: true,
-    env: 'production',
-  },
-]
-
-const entryPoints: EntryPointOptions[] = [
-  {
-    prefix: 'redux-toolkit',
-    folder: '',
-    entryPoint: 'src/index.ts',
-    extractionConfig: 'api-extractor.json',
-    globalName: 'RTK',
-  },
-  {
-    prefix: 'rtk-query',
-    folder: 'query',
-    entryPoint: 'src/query/index.ts',
-    extractionConfig: 'api-extractor.query.json',
-    globalName: 'RTKQ',
-  },
-  {
-    prefix: 'rtk-query-react',
-    folder: 'query/react',
-    entryPoint: 'src/query/react/index.ts',
-    extractionConfig: 'api-extractor.query-react.json',
-    globalName: 'RTKQ',
-  },
-]
-
-const esVersionMappings = {
-  // Don't output ES2015 - have TS convert to ES5 instead
-  es2015: ts.ScriptTarget.ES5,
-  es2017: ts.ScriptTarget.ES2017,
-  es2018: ts.ScriptTarget.ES2018,
-  es2019: ts.ScriptTarget.ES2019,
-  es2020: ts.ScriptTarget.ES2020,
-}
-
-async function bundle(options: BuildOptions & EntryPointOptions) {
-  const {
-    format,
-    minify,
-    env,
-    folder = '',
-    prefix = 'redux-toolkit',
-    name,
-    target = 'es2015',
-    entryPoint,
-  } = options
-
-  const outputFolder = path.join('dist', folder)
-  const outputFilename = `${prefix}.${name}.js`
-  const outputFilePath = path.join(outputFolder, outputFilename)
-
-  const result = await build({
-    entryPoints: [entryPoint],
-    outfile: outputFilePath,
-    write: false,
-    target: target,
-    sourcemap: 'inline',
-    bundle: true,
-    format: format === 'umd' ? 'esm' : format,
-    // Needed to prevent auto-replacing of process.env.NODE_ENV in all builds
-    platform: 'neutral',
-    // Needed to return to normal lookup behavior when platform: 'neutral'
-    mainFields: ['browser', 'module', 'main'],
-    conditions: ['browser'],
-    define: env
-      ? {
-          'process.env.NODE_ENV': JSON.stringify(env),
-        }
-      : {},
-    plugins: [
-      {
-        name: 'node_module_external',
-        setup(build) {
-          build.onResolve({ filter: /.*/ }, (args) => {
-            if (format === 'umd') {
-              return
-            }
-            if (args.path.startsWith('.') || args.path.startsWith('/')) {
-              return undefined
-            } else {
-              return {
-                path: args.path,
-                external: true,
-              }
-            }
-          })
-          build.onLoad({ filter: /getDefaultMiddleware/ }, async (args) => {
-            if (env !== 'production' || format !== 'umd') {
-              return
-            }
-            const source = await fs.readFile(args.path, 'utf-8')
-            const defaultPattern =
-              /\/\* PROD_START_REMOVE_UMD[\s\S]*?\/\* PROD_STOP_REMOVE_UMD \*\//g
-            const code = source.replace(defaultPattern, '')
-            return {
-              contents: code,
-              loader: 'ts',
-            }
-          })
-        },
-      },
-    ],
-  })
-
-  for (const chunk of result.outputFiles) {
-    const esVersion =
-      target in esVersionMappings
-        ? esVersionMappings[target]
-        : ts.ScriptTarget.ES5
-
-    const origin = chunk.text
-    const sourcemap = extractInlineSourcemap(origin)
-    const result = ts.transpileModule(removeInlineSourceMap(origin), {
-      fileName: chunk.path,
-      compilerOptions: {
-        sourceMap: true,
-        module:
-          format !== 'cjs' ? ts.ModuleKind.ES2015 : ts.ModuleKind.CommonJS,
-        target: esVersion,
-      },
-    })
-
-    const mergedSourcemap = merge(sourcemap, result.sourceMapText)
-    let code = result.outputText
-    let mapping: RawSourceMap = mergedSourcemap
-
-    if (minify) {
-      const transformResult = await terser.minify(
-        appendInlineSourceMap(code, mapping),
-        {
-          sourceMap: {
-            content: 'inline',
-            asObject: true,
-            url: path.basename(chunk.path) + '.map',
-          } as any,
-          output: {
-            comments: false,
-          },
-          compress: {
-            keep_infinity: true,
-            pure_getters: true,
-            passes: 10,
-          },
-          ecma: 5,
-          toplevel: true,
-        }
-      )
-      code = transformResult.code
-      mapping = transformResult.map as RawSourceMap
-    }
-
-    const relativePath = path.relative(process.cwd(), chunk.path)
-    console.log(`Build artifact: ${relativePath}, settings: `, {
-      target,
-      output: ts.ScriptTarget[esVersion],
-    })
-    await fs.writeFile(chunk.path, code)
-    await fs.writeJSON(chunk.path + '.map', mapping)
-  }
-}
-
-/**
- * since esbuild doesn't support umd, we use rollup to convert esm to umd
- */
-async function buildUMD(
-  outputPath: string,
-  prefix: string,
-  globalName: string
-) {
-  for (let umdExtension of ['umd', 'umd.min']) {
-    const input = path.join(outputPath, `${prefix}.${umdExtension}.js`)
-    const instance = await rollup.rollup({
-      input: [input],
-      onwarn(warning, warn) {
-        if (warning.code === 'THIS_IS_UNDEFINED') return
-        warn(warning) // this requires Rollup 0.46
-      },
-    })
-    await instance.write({
-      format: 'umd',
-      name: globalName,
-      file: input,
-      sourcemap: true,
-      globals: {
-        // These packages have specific global names from their UMD bundles
-        react: 'React',
-        'react-redux': 'ReactRedux',
-      },
-    })
-  }
-}
-
-// Generates an index file to handle importing CJS dev/prod
-async function writeEntry(folder: string, prefix: string) {
-  await fs.writeFile(
-    path.join('dist', folder, 'index.js'),
-    `'use strict'
-if (process.env.NODE_ENV === 'production') {
-  module.exports = require('./${prefix}.cjs.production.min.js')
-} else {
-  module.exports = require('./${prefix}.cjs.development.js')
-}`
-  )
-}
-
-interface BuildArgs {
-  skipExtraction?: boolean
-  local: boolean
-}
-
-async function main({ skipExtraction = false, local = false }: BuildArgs) {
-  // Dist folder will be removed by rimraf beforehand so TSC can generate typedefs
-  await fs.ensureDir(outputDir)
-
-  for (let entryPoint of entryPoints) {
-    const { folder, prefix } = entryPoint
-    const outputPath = path.join('dist', folder)
-    fs.ensureDirSync(outputPath)
-
-    // Run builds in parallel
-    const bundlePromises = buildTargets.map((options) =>
-      bundle({
-        ...options,
-        ...entryPoint,
-      })
-    )
-    await Promise.all(bundlePromises)
-    await writeEntry(folder, prefix)
-  }
-
-  // Run UMD builds after everything else so we don't have to sleep after each set
-  for (let entryPoint of entryPoints) {
-    const { folder } = entryPoint
-    const outputPath = path.join('dist', folder)
-    await buildUMD(outputPath, entryPoint.prefix, entryPoint.globalName)
-  }
-
-  if (!skipExtraction) {
-    for (let entryPoint of entryPoints) {
-      try {
-        // Load and parse the api-extractor.json file
-        const extractorConfig: ExtractorConfig =
-          ExtractorConfig.loadFileAndPrepare(entryPoint.extractionConfig)
-
-        console.log('Extracting API types for entry point: ', entryPoint.prefix)
-        // Invoke API Extractor
-        const extractorResult: ExtractorResult = Extractor.invoke(
-          extractorConfig,
-          {
-            // Equivalent to the "--local" command-line parameter
-            localBuild: local,
-
-            // Equivalent to the "--verbose" command-line parameter
-            showVerboseMessages: false,
-          }
-        )
-
-        if (extractorResult.succeeded) {
-          console.log(`API Extractor completed successfully`)
-        } else {
-          console.error(
-            `API Extractor completed with ${extractorResult.errorCount} errors` +
-              ` and ${extractorResult.warningCount} warnings`
-          )
-        }
-      } catch (e) {
-        console.error('API extractor crashed: ', e)
-      }
-    }
-  }
-}
-
-const { skipExtraction, local } = argv
-main({ skipExtraction, local })
diff --git a/packages/toolkit/scripts/cli.js b/packages/toolkit/scripts/cli.js
deleted file mode 100644
--- a/packages/toolkit/scripts/cli.js
+++ /dev/null
@@ -1,2 +0,0 @@
-require('./register') // must be the first
-require('./build.ts')
diff --git a/packages/toolkit/scripts/mangleErrors.cjs b/packages/toolkit/scripts/mangleErrors.cjs
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/scripts/mangleErrors.cjs
@@ -0,0 +1,159 @@
+const fs = require('fs')
+const path = require('path')
+const helperModuleImports = require('@babel/helper-module-imports')
+
+/**
+ * Converts an AST type into a javascript string so that it can be added to the error message lookup.
+ *
+ * Adapted from React (https://github.com/facebook/react/blob/master/scripts/shared/evalToString.js) with some
+ * adjustments
+ */
+const evalToString = (ast) => {
+  switch (ast.type) {
+    case 'StringLiteral':
+    case 'Literal': // ESLint
+      return ast.value
+    case 'BinaryExpression': // `+`
+      if (ast.operator !== '+') {
+        throw new Error('Unsupported binary operator ' + ast.operator)
+      }
+      return evalToString(ast.left) + evalToString(ast.right)
+    case 'TemplateLiteral':
+      return ast.quasis.reduce(
+        (concatenatedValue, templateElement) =>
+          concatenatedValue + templateElement.value.raw,
+        ''
+      )
+    case 'Identifier':
+      return ast.name
+    default:
+      console.log('Bad AST in mangleErrors -> evalToString(): ', ast)
+      throw new Error(`Unsupported AST in evalToString: ${ast.type}, ${ast}`)
+  }
+}
+
+/**
+ * Takes a `throw new error` statement and transforms it depending on the minify argument. Either option results in a
+ * smaller bundle size in production for consumers.
+ *
+ * If minify is enabled, we'll replace the error message with just an index that maps to an arrow object lookup.
+ *
+ * If minify is disabled, we'll add in a conditional statement to check the process.env.NODE_ENV which will output a
+ * an error number index in production or the actual error message in development. This allows consumers using webpack
+ * or another build tool to have these messages in development but have just the error index in production.
+ *
+ * E.g.
+ *  Before:
+ *    throw new Error("This is my error message.");
+ *    throw new Error("This is a second error message.");
+ *
+ *  After (with minify):
+ *    throw new Error(0);
+ *    throw new Error(1);
+ *
+ *  After: (without minify):
+ *    throw new Error(node.process.NODE_ENV === 'production' ? 0 : "This is my error message.");
+ *    throw new Error(node.process.NODE_ENV === 'production' ? 1 : "This is a second error message.");
+ */
+module.exports = (babel) => {
+  const t = babel.types
+  // When the plugin starts up, we'll load in the existing file. This allows us to continually add to it so that the
+  // indexes do not change between builds.
+  let errorsFiles = ''
+  // Save this to the root
+  const errorsPath = path.join(__dirname, '../../../errors.json')
+  if (fs.existsSync(errorsPath)) {
+    errorsFiles = fs.readFileSync(errorsPath).toString()
+  }
+  let errors = Object.values(JSON.parse(errorsFiles || '{}'))
+  // This variable allows us to skip writing back to the file if the errors array hasn't changed
+  let changeInArray = false
+
+  return {
+    pre: () => {
+      changeInArray = false
+    },
+    visitor: {
+      ThrowStatement(path, file) {
+        const args = path.node.argument.arguments
+        const minify = file.opts.minify
+
+        if (args && args[0]) {
+          // Skip running this logic when certain types come up:
+          //  Identifier comes up when a variable is thrown (E.g. throw new error(message))
+          //  NumericLiteral, CallExpression, and ConditionalExpression is code we have already processed
+          if (
+            path.node.argument.arguments[0].type === 'Identifier' ||
+            path.node.argument.arguments[0].type === 'NumericLiteral' ||
+            path.node.argument.arguments[0].type === 'ConditionalExpression' ||
+            path.node.argument.arguments[0].type === 'CallExpression' ||
+            path.node.argument.arguments[0].type === 'ObjectExpression' ||
+            path.node.argument.arguments[0].type === 'MemberExpression' ||
+            path.node.argument.arguments[0]?.callee?.name === 'HandledError'
+          ) {
+            return
+          }
+
+          const errorMsgLiteral = evalToString(path.node.argument.arguments[0])
+
+          if (errorMsgLiteral.includes('Super expression')) {
+            // ignore Babel runtime error message
+            return
+          }
+
+          // Attempt to get the existing index of the error. If it is not found, add it to the array as a new error.
+          let errorIndex = errors.indexOf(errorMsgLiteral)
+          if (errorIndex === -1) {
+            errors.push(errorMsgLiteral)
+            errorIndex = errors.length - 1
+            changeInArray = true
+          }
+
+          // Import the error message function
+          const formatProdErrorMessageIdentifier = helperModuleImports.addNamed(
+            path,
+            'formatProdErrorMessage',
+            '@reduxjs/toolkit',
+            { nameHint: 'formatProdErrorMessage' }
+          )
+
+          // Creates a function call to output the message to the error code page on the website
+          const prodMessage = t.callExpression(
+            formatProdErrorMessageIdentifier,
+            [t.numericLiteral(errorIndex)]
+          )
+
+          if (minify) {
+            path.replaceWith(
+              t.throwStatement(
+                t.newExpression(t.identifier('Error'), [prodMessage])
+              )
+            )
+          } else {
+            path.replaceWith(
+              t.throwStatement(
+                t.newExpression(t.identifier('Error'), [
+                  t.conditionalExpression(
+                    t.binaryExpression(
+                      '===',
+                      t.identifier('process.env.NODE_ENV'),
+                      t.stringLiteral('production')
+                    ),
+                    prodMessage,
+                    path.node.argument.arguments[0]
+                  ),
+                ])
+              )
+            )
+          }
+        }
+      },
+    },
+    post: () => {
+      // If there is a new error in the array, convert it to an indexed object and write it back to the file.
+      if (changeInArray) {
+        fs.writeFileSync(errorsPath, JSON.stringify({ ...errors }, null, 2))
+      }
+    },
+  }
+}
diff --git a/packages/toolkit/scripts/moduld.d.ts b/packages/toolkit/scripts/moduld.d.ts
deleted file mode 100644
--- a/packages/toolkit/scripts/moduld.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-declare module 'merge-source-map' {
-  import type { RawSourceMap } from 'source-map'
-  export default function merge(
-    map1: string | RawSourceMap,
-    map2: string | RawSourceMap
-  ): RawSourceMap
-}
diff --git a/packages/toolkit/scripts/register.js b/packages/toolkit/scripts/register.js
deleted file mode 100644
--- a/packages/toolkit/scripts/register.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const esbuild = require('esbuild')
-const fs = require('fs')
-require.extensions['.ts'] = (mod, filename) => {
-  const ts = fs.readFileSync(filename, 'utf-8')
-  const { code } = esbuild.transformSync(ts, {
-    loader: 'ts',
-    target: 'es2017',
-    format: 'cjs',
-  })
-  mod._compile(code, filename)
-}
diff --git a/packages/toolkit/scripts/sourcemap.ts b/packages/toolkit/scripts/sourcemap.ts
deleted file mode 100644
--- a/packages/toolkit/scripts/sourcemap.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { fromObject, fromComment } from 'convert-source-map'
-const SOURCEMAPPING_URL = 'sourceMappingURL'
-
-const SOURCEMAP_REG = new RegExp(
-  `^\\/\\/#\\s+${SOURCEMAPPING_URL}=.+\\n?`,
-  'gm'
-)
-function appendInlineSourceMap(code: string, sourceMap: any) {
-  if (sourceMap) {
-    const mapping = fromObject(sourceMap)
-    return `${code}\n${mapping.toComment()}`
-  } else {
-    return code
-  }
-}
-function removeInlineSourceMap(code) {
-  return code.replace(
-    new RegExp(`^\\/\\/#\\s+${SOURCEMAPPING_URL}=.+\\n?`, 'gm'),
-    ''
-  )
-}
-function extractInlineSourcemap(code: string) {
-  return fromComment(code.match(SOURCEMAP_REG)?.[0]).toObject()
-}
-function getLocation(
-  source: string,
-  search: string
-): { line: number; column: number } {
-  const outIndex = source.indexOf(search)
-  if (outIndex < 0) {
-    throw new Error(`Failed to find ${search} in output`)
-  }
-  const outLines = source.slice(0, outIndex).split('\n')
-  const outLine = outLines.length
-  const outColumn = outLines[outLines.length - 1].length
-  return {
-    line: outLine,
-    column: outColumn,
-  }
-}
-
-export {
-  extractInlineSourcemap,
-  removeInlineSourceMap,
-  appendInlineSourceMap,
-  getLocation,
-}
diff --git a/packages/toolkit/scripts/types.ts b/packages/toolkit/scripts/types.ts
deleted file mode 100644
--- a/packages/toolkit/scripts/types.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-export interface BuildOptions {
-  format: 'cjs' | 'umd' | 'esm'
-  name:
-    | 'cjs.development'
-    | 'cjs.production.min'
-    | 'esm'
-    | 'modern'
-    | 'modern.development'
-    | 'modern.production.min'
-    | 'umd'
-    | 'umd.min'
-  minify: boolean
-  env: 'development' | 'production' | ''
-  target?: 'es2017'
-}
-
-export interface EntryPointOptions {
-  prefix: string
-  folder: string
-  entryPoint: string
-  extractionConfig: string
-  // globalName is used in the conversion to umd files to separate rtk from rtk-query on a global namespace
-  globalName: string
-}
diff --git a/packages/toolkit/scripts/writeGitVersion.js b/packages/toolkit/scripts/writeGitVersion.js
deleted file mode 100644
--- a/packages/toolkit/scripts/writeGitVersion.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const path = require('path')
-const fs = require('fs')
-
-const gitRev = process.argv[2]
-
-const packagePath = path.join(__dirname, '../package.json')
-const pkg = require(packagePath)
-
-pkg.version = `${pkg.version}-${gitRev}`
-fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2))
diff --git a/packages/toolkit/scripts/writeGitVersion.mjs b/packages/toolkit/scripts/writeGitVersion.mjs
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/scripts/writeGitVersion.mjs
@@ -0,0 +1,14 @@
+import path from 'path'
+import fs from 'fs'
+import { fileURLToPath } from 'node:url'
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = path.dirname(__filename)
+
+const gitRev = process.argv[2]
+
+const packagePath = path.join(__dirname, '../package.json')
+const pkg = JSON.parse(fs.readFileSync(packagePath))
+
+pkg.version = `${pkg.version}-${gitRev}`
+fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2))
diff --git a/packages/toolkit/src/autoBatchEnhancer.ts b/packages/toolkit/src/autoBatchEnhancer.ts
--- a/packages/toolkit/src/autoBatchEnhancer.ts
+++ b/packages/toolkit/src/autoBatchEnhancer.ts
@@ -9,26 +9,6 @@ export const prepareAutoBatched =
     meta: { [SHOULD_AUTOBATCH]: true },
   })
 
-// TODO Remove this in 2.0
-// Copied from https://github.com/feross/queue-microtask
-let promise: Promise<any>
-const queueMicrotaskShim =
-  typeof queueMicrotask === 'function'
-    ? queueMicrotask.bind(
-        typeof window !== 'undefined'
-          ? window
-          : typeof global !== 'undefined'
-          ? global
-          : globalThis
-      )
-    : // reuse resolved promise, and allocate it lazily
-      (cb: () => void) =>
-        (promise || (promise = Promise.resolve())).then(cb).catch((err: any) =>
-          setTimeout(() => {
-            throw err
-          }, 0)
-        )
-
 const createQueueWithTimer = (timeout: number) => {
   return (notify: () => void) => {
     setTimeout(notify, timeout)
@@ -63,10 +43,10 @@ export type AutoBatchOptions =
  *
  * By default, it will queue a notification for the end of the event loop tick.
  * However, you can pass several other options to configure the behavior:
- * - `{type: 'tick'}: queues using `queueMicrotask` (default)
+ * - `{type: 'tick'}`: queues using `queueMicrotask`
  * - `{type: 'timer, timeout: number}`: queues using `setTimeout`
- * - `{type: 'raf'}`: queues using `requestAnimationFrame`
- * - `{type: 'callback', queueNotification: (notify: () => void) => void}: lets you provide your own callback
+ * - `{type: 'raf'}`: queues using `requestAnimationFrame` (default)
+ * - `{type: 'callback', queueNotification: (notify: () => void) => void}`: lets you provide your own callback
  *
  *
  */
@@ -84,7 +64,7 @@ export const autoBatchEnhancer =
 
     const queueCallback =
       options.type === 'tick'
-        ? queueMicrotaskShim
+        ? queueMicrotask
         : options.type === 'raf'
         ? rAF
         : options.type === 'callback'
diff --git a/packages/toolkit/src/combineSlices.ts b/packages/toolkit/src/combineSlices.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/combineSlices.ts
@@ -0,0 +1,441 @@
+import type { UnknownAction, Reducer, StateFromReducersMapObject } from 'redux'
+import { combineReducers } from 'redux'
+import { nanoid } from './nanoid'
+import type {
+  Id,
+  NonUndefined,
+  Tail,
+  UnionToIntersection,
+  WithOptionalProp,
+} from './tsHelpers'
+import { emplace } from './utils'
+
+type SliceLike<ReducerPath extends string, State> = {
+  reducerPath: ReducerPath
+  reducer: Reducer<State>
+}
+
+type AnySliceLike = SliceLike<string, any>
+
+type SliceLikeReducerPath<A extends AnySliceLike> = A extends SliceLike<
+  infer ReducerPath,
+  any
+>
+  ? ReducerPath
+  : never
+
+type SliceLikeState<A extends AnySliceLike> = A extends SliceLike<
+  any,
+  infer State
+>
+  ? State
+  : never
+
+export type WithSlice<A extends AnySliceLike> = {
+  [Path in SliceLikeReducerPath<A>]: SliceLikeState<A>
+}
+
+type ReducerMap = Record<string, Reducer>
+
+type ExistingSliceLike<DeclaredState> = {
+  [ReducerPath in keyof DeclaredState]: SliceLike<
+    ReducerPath & string,
+    NonUndefined<DeclaredState[ReducerPath]>
+  >
+}[keyof DeclaredState]
+
+export type InjectConfig = {
+  /**
+   * Allow replacing reducer with a different reference. Normally, an error will be thrown if a different reducer instance to the one already injected is used.
+   */
+  overrideExisting?: boolean
+}
+
+/**
+ * A reducer that allows for slices/reducers to be injected after initialisation.
+ */
+export interface CombinedSliceReducer<
+  InitialState,
+  DeclaredState = InitialState
+> extends Reducer<DeclaredState, UnknownAction, Partial<DeclaredState>> {
+  /**
+   * Provide a type for slices that will be injected lazily.
+   *
+   * One way to do this would be with interface merging:
+   * ```ts
+   *
+   * export interface LazyLoadedSlices {}
+   *
+   * export const rootReducer = combineSlices(stringSlice).withLazyLoadedSlices<LazyLoadedSlices>();
+   *
+   * // elsewhere
+   *
+   * declare module './reducer' {
+   *   export interface LazyLoadedSlices extends WithSlice<typeof booleanSlice> {}
+   * }
+   *
+   * const withBoolean = rootReducer.inject(booleanSlice);
+   *
+   * // elsewhere again
+   *
+   * declare module './reducer' {
+   *   export interface LazyLoadedSlices {
+   *     customName: CustomState
+   *   }
+   * }
+   *
+   * const withCustom = rootReducer.inject({ reducerPath: "customName", reducer: customSlice.reducer })
+   * ```
+   */
+  withLazyLoadedSlices<Lazy = {}>(): CombinedSliceReducer<
+    InitialState,
+    Id<DeclaredState & Partial<Lazy>>
+  >
+
+  /**
+   * Inject a slice.
+   *
+   * Accepts an individual slice, RTKQ API instance, or a "slice-like" { reducerPath, reducer } object.
+   *
+   * ```ts
+   * rootReducer.inject(booleanSlice)
+   * rootReducer.inject(baseApi)
+   * rootReducer.inject({ reducerPath: 'boolean' as const, reducer: newReducer }, { overrideExisting: true })
+   * ```
+   *
+   */
+  inject<Sl extends Id<ExistingSliceLike<DeclaredState>>>(
+    slice: Sl,
+    config?: InjectConfig
+  ): CombinedSliceReducer<InitialState, Id<DeclaredState & WithSlice<Sl>>>
+
+  /**
+   * Inject a slice.
+   *
+   * Accepts an individual slice, RTKQ API instance, or a "slice-like" { reducerPath, reducer } object.
+   *
+   * ```ts
+   * rootReducer.inject(booleanSlice)
+   * rootReducer.inject(baseApi)
+   * rootReducer.inject({ reducerPath: 'boolean' as const, reducer: newReducer }, { overrideExisting: true })
+   * ```
+   *
+   */
+  inject<ReducerPath extends string, State>(
+    slice: SliceLike<
+      ReducerPath,
+      State & (ReducerPath extends keyof DeclaredState ? never : State)
+    >,
+    config?: InjectConfig
+  ): CombinedSliceReducer<
+    InitialState,
+    Id<DeclaredState & WithSlice<SliceLike<ReducerPath, String>>>
+  >
+
+  /**
+   * Create a selector that guarantees that the slices injected will have a defined value when selector is run.
+   *
+   * ```ts
+   * const selectBooleanWithoutInjection = (state: RootState) => state.boolean;
+   * //                                                                ^? boolean | undefined
+   *
+   * const selectBoolean = rootReducer.inject(booleanSlice).selector((state) => {
+   *   // if action hasn't been dispatched since slice was injected, this would usually be undefined
+   *   // however selector() uses a Proxy around the first parameter to ensure that it evaluates to the initial state instead, if undefined
+   *   return state.boolean;
+   *   //           ^? boolean
+   * })
+   * ```
+   *
+   * If the reducer is nested inside the root state, a selectState callback can be passed to retrieve the reducer's state.
+   *
+   * ```ts
+   *
+   * export interface LazyLoadedSlices {};
+   *
+   * export const innerReducer = combineSlices(stringSlice).withLazyLoadedSlices<LazyLoadedSlices>();
+   *
+   * export const rootReducer = combineSlices({ inner: innerReducer });
+   *
+   * export type RootState = ReturnType<typeof rootReducer>;
+   *
+   * // elsewhere
+   *
+   * declare module "./reducer.ts" {
+   *  export interface LazyLoadedSlices extends WithSlice<typeof booleanSlice> {}
+   * }
+   *
+   * const withBool = innerReducer.inject(booleanSlice);
+   *
+   * const selectBoolean = withBool.selector(
+   *   (state) => state.boolean,
+   *   (rootState: RootState) => state.inner
+   * );
+   * //    now expects to be passed RootState instead of innerReducer state
+   *
+   * ```
+   *
+   * Value passed to selectorFn will be a Proxy - use selector.original(proxy) to get original state value (useful for debugging)
+   *
+   * ```ts
+   * const injectedReducer = rootReducer.inject(booleanSlice);
+   * const selectBoolean = injectedReducer.selector((state) => {
+   *   console.log(injectedReducer.selector.original(state).boolean) // possibly undefined
+   *   return state.boolean
+   * })
+   * ```
+   */
+  selector: {
+    /**
+     * Create a selector that guarantees that the slices injected will have a defined value when selector is run.
+     *
+     * ```ts
+     * const selectBooleanWithoutInjection = (state: RootState) => state.boolean;
+     * //                                                                ^? boolean | undefined
+     *
+     * const selectBoolean = rootReducer.inject(booleanSlice).selector((state) => {
+     *   // if action hasn't been dispatched since slice was injected, this would usually be undefined
+     *   // however selector() uses a Proxy around the first parameter to ensure that it evaluates to the initial state instead, if undefined
+     *   return state.boolean;
+     *   //           ^? boolean
+     * })
+     * ```
+     *
+     * Value passed to selectorFn will be a Proxy - use selector.original(proxy) to get original state value (useful for debugging)
+     *
+     * ```ts
+     * const injectedReducer = rootReducer.inject(booleanSlice);
+     * const selectBoolean = injectedReducer.selector((state) => {
+     *   console.log(injectedReducer.selector.original(state).boolean) // undefined
+     *   return state.boolean
+     * })
+     * ```
+     */
+    <Selector extends (state: DeclaredState, ...args: any[]) => unknown>(
+      selectorFn: Selector
+    ): (
+      state: WithOptionalProp<
+        Parameters<Selector>[0],
+        Exclude<keyof DeclaredState, keyof InitialState>
+      >,
+      ...args: Tail<Parameters<Selector>>
+    ) => ReturnType<Selector>
+
+    /**
+     * Create a selector that guarantees that the slices injected will have a defined value when selector is run.
+     *
+     * ```ts
+     * const selectBooleanWithoutInjection = (state: RootState) => state.boolean;
+     * //                                                                ^? boolean | undefined
+     *
+     * const selectBoolean = rootReducer.inject(booleanSlice).selector((state) => {
+     *   // if action hasn't been dispatched since slice was injected, this would usually be undefined
+     *   // however selector() uses a Proxy around the first parameter to ensure that it evaluates to the initial state instead, if undefined
+     *   return state.boolean;
+     *   //           ^? boolean
+     * })
+     * ```
+     *
+     * If the reducer is nested inside the root state, a selectState callback can be passed to retrieve the reducer's state.
+     *
+     * ```ts
+     *
+     * interface LazyLoadedSlices {};
+     *
+     * const innerReducer = combineSlices(stringSlice).withLazyLoadedSlices<LazyLoadedSlices>();
+     *
+     * const rootReducer = combineSlices({ inner: innerReducer });
+     *
+     * type RootState = ReturnType<typeof rootReducer>;
+     *
+     * // elsewhere
+     *
+     * declare module "./reducer.ts" {
+     *  interface LazyLoadedSlices extends WithSlice<typeof booleanSlice> {}
+     * }
+     *
+     * const withBool = innerReducer.inject(booleanSlice);
+     *
+     * const selectBoolean = withBool.selector(
+     *   (state) => state.boolean,
+     *   (rootState: RootState) => state.inner
+     * );
+     * //    now expects to be passed RootState instead of innerReducer state
+     *
+     * ```
+     *
+     * Value passed to selectorFn will be a Proxy - use selector.original(proxy) to get original state value (useful for debugging)
+     *
+     * ```ts
+     * const injectedReducer = rootReducer.inject(booleanSlice);
+     * const selectBoolean = injectedReducer.selector((state) => {
+     *   console.log(injectedReducer.selector.original(state).boolean) // possibly undefined
+     *   return state.boolean
+     * })
+     * ```
+     */
+    <
+      Selector extends (state: DeclaredState, ...args: any[]) => unknown,
+      RootState
+    >(
+      selectorFn: Selector,
+      selectState: (
+        rootState: RootState,
+        ...args: Tail<Parameters<Selector>>
+      ) => WithOptionalProp<
+        Parameters<Selector>[0],
+        Exclude<keyof DeclaredState, keyof InitialState>
+      >
+    ): (
+      state: RootState,
+      ...args: Tail<Parameters<Selector>>
+    ) => ReturnType<Selector>
+    /**
+     * Returns the unproxied state. Useful for debugging.
+     * @param state state Proxy, that ensures injected reducers have value
+     * @returns original, unproxied state
+     * @throws if value passed is not a state Proxy
+     */
+    original: (state: DeclaredState) => InitialState & Partial<DeclaredState>
+  }
+}
+
+type InitialState<Slices extends Array<AnySliceLike | ReducerMap>> =
+  UnionToIntersection<
+    Slices[number] extends infer Slice
+      ? Slice extends AnySliceLike
+        ? WithSlice<Slice>
+        : StateFromReducersMapObject<Slice>
+      : never
+  >
+
+const isSliceLike = (
+  maybeSliceLike: AnySliceLike | ReducerMap
+): maybeSliceLike is AnySliceLike =>
+  'reducerPath' in maybeSliceLike &&
+  typeof maybeSliceLike.reducerPath === 'string'
+
+const getReducers = (slices: Array<AnySliceLike | ReducerMap>) =>
+  slices.flatMap((sliceOrMap) =>
+    isSliceLike(sliceOrMap)
+      ? [[sliceOrMap.reducerPath, sliceOrMap.reducer] as const]
+      : Object.entries(sliceOrMap)
+  )
+
+const ORIGINAL_STATE = Symbol.for('rtk-state-proxy-original')
+
+const isStateProxy = (value: any) => !!value && !!value[ORIGINAL_STATE]
+
+const stateProxyMap = new WeakMap<object, object>()
+
+const createStateProxy = <State extends object>(
+  state: State,
+  reducerMap: Partial<Record<string, Reducer>>
+) =>
+  emplace(stateProxyMap, state, {
+    insert: () =>
+      new Proxy(state, {
+        get: (target, prop, receiver) => {
+          if (prop === ORIGINAL_STATE) return target
+          const result = Reflect.get(target, prop, receiver)
+          if (typeof result === 'undefined') {
+            const reducer = reducerMap[prop.toString()]
+            if (reducer) {
+              // ensure action type is random, to prevent reducer treating it differently
+              const reducerResult = reducer(undefined, { type: nanoid() })
+              if (typeof reducerResult === 'undefined') {
+                throw new Error(
+                  `The slice reducer for key "${prop.toString()}" returned undefined when called for selector(). ` +
+                    `If the state passed to the reducer is undefined, you must ` +
+                    `explicitly return the initial state. The initial state may ` +
+                    `not be undefined. If you don't want to set a value for this reducer, ` +
+                    `you can use null instead of undefined.`
+                )
+              }
+              return reducerResult
+            }
+          }
+          return result
+        },
+      }),
+  }) as State
+
+const original = (state: any) => {
+  if (!isStateProxy(state)) {
+    throw new Error('original must be used on state Proxy')
+  }
+  return state[ORIGINAL_STATE]
+}
+
+export function combineSlices<
+  Slices extends [
+    AnySliceLike | ReducerMap,
+    ...Array<AnySliceLike | ReducerMap>
+  ]
+>(...slices: Slices): CombinedSliceReducer<Id<InitialState<Slices>>> {
+  const reducerMap = Object.fromEntries<Reducer>(getReducers(slices))
+
+  const getReducer = () => combineReducers(reducerMap)
+
+  let reducer = getReducer()
+
+  function combinedReducer(
+    state: Record<string, unknown>,
+    action: UnknownAction
+  ) {
+    return reducer(state, action)
+  }
+
+  combinedReducer.withLazyLoadedSlices = () => combinedReducer
+
+  const inject = (
+    slice: AnySliceLike,
+    config: InjectConfig = {}
+  ): typeof combinedReducer => {
+    const { reducerPath, reducer: reducerToInject } = slice
+
+    const currentReducer = reducerMap[reducerPath]
+    if (
+      !config.overrideExisting &&
+      currentReducer &&
+      currentReducer !== reducerToInject
+    ) {
+      if (
+        typeof process !== 'undefined' &&
+        process.env.NODE_ENV === 'development'
+      ) {
+        console.error(
+          `called \`inject\` to override already-existing reducer ${reducerPath} without specifying \`overrideExisting: true\``
+        )
+      }
+
+      return combinedReducer
+    }
+
+    reducerMap[reducerPath] = reducerToInject
+
+    reducer = getReducer()
+
+    return combinedReducer
+  }
+
+  const selector = Object.assign(
+    function makeSelector<State extends object, RootState, Args extends any[]>(
+      selectorFn: (state: State, ...args: Args) => any,
+      selectState?: (rootState: RootState, ...args: Args) => State
+    ) {
+      return function selector(state: State, ...args: Args) {
+        return selectorFn(
+          createStateProxy(
+            selectState ? selectState(state as any, ...args) : state,
+            reducerMap
+          ),
+          ...args
+        )
+      }
+    },
+    { original }
+  )
+
+  return Object.assign(combinedReducer, { inject, selector }) as any
+}
diff --git a/packages/toolkit/src/configureStore.ts b/packages/toolkit/src/configureStore.ts
--- a/packages/toolkit/src/configureStore.ts
+++ b/packages/toolkit/src/configureStore.ts
@@ -3,42 +3,36 @@ import type {
   ReducersMapObject,
   Middleware,
   Action,
-  AnyAction,
   StoreEnhancer,
   Store,
-  Dispatch,
-  PreloadedState,
-  CombinedState,
+  UnknownAction,
+} from 'redux'
+import {
+  applyMiddleware,
+  createStore,
+  compose,
+  combineReducers,
+  isPlainObject,
 } from 'redux'
-import { createStore, compose, applyMiddleware, combineReducers } from 'redux'
 import type { DevToolsEnhancerOptions as DevToolsOptions } from './devtoolsExtension'
 import { composeWithDevTools } from './devtoolsExtension'
 
-import isPlainObject from './isPlainObject'
 import type {
   ThunkMiddlewareFor,
-  CurriedGetDefaultMiddleware,
+  GetDefaultMiddleware,
 } from './getDefaultMiddleware'
-import { curryGetDefaultMiddleware } from './getDefaultMiddleware'
+import { buildGetDefaultMiddleware } from './getDefaultMiddleware'
 import type {
-  NoInfer,
   ExtractDispatchExtensions,
   ExtractStoreExtensions,
   ExtractStateExtensions,
 } from './tsHelpers'
-import { EnhancerArray } from './utils'
+import type { Tuple } from './utils'
+import type { GetDefaultEnhancers } from './getDefaultEnhancers'
+import { buildGetDefaultEnhancers } from './getDefaultEnhancers'
 
 const IS_PRODUCTION = process.env.NODE_ENV === 'production'
 
-/**
- * Callback function type, to be used in `ConfigureStoreOptions.enhancers`
- *
- * @public
- */
-export type ConfigureEnhancersCallback<E extends Enhancers = Enhancers> = (
-  defaultEnhancers: EnhancerArray<[StoreEnhancer<{}, {}>]>
-) => E
-
 /**
  * Options for `configureStore()`.
  *
@@ -46,24 +40,25 @@ export type ConfigureEnhancersCallback<E extends Enhancers = Enhancers> = (
  */
 export interface ConfigureStoreOptions<
   S = any,
-  A extends Action = AnyAction,
-  M extends Middlewares<S> = Middlewares<S>,
-  E extends Enhancers = Enhancers
+  A extends Action = UnknownAction,
+  M extends Tuple<Middlewares<S>> = Tuple<Middlewares<S>>,
+  E extends Tuple<Enhancers> = Tuple<Enhancers>,
+  P = S
 > {
   /**
    * A single reducer function that will be used as the root reducer, or an
    * object of slice reducers that will be passed to `combineReducers()`.
    */
-  reducer: Reducer<S, A> | ReducersMapObject<S, A>
+  reducer: Reducer<S, A, P> | ReducersMapObject<S, A, P>
 
   /**
-   * An array of Redux middleware to install. If not supplied, defaults to
-   * the set of middleware returned by `getDefaultMiddleware()`.
+   * An array of Redux middleware to install, or a callback receiving `getDefaultMiddleware` and returning a Tuple of middleware.
+   * If not supplied, defaults to the set of middleware returned by `getDefaultMiddleware()`.
    *
    * @example `middleware: (gDM) => gDM().concat(logger, apiMiddleware, yourCustomMiddleware)`
    * @see https://redux-toolkit.js.org/api/getDefaultMiddleware#intended-usage
    */
-  middleware?: ((getDefaultMiddleware: CurriedGetDefaultMiddleware<S>) => M) | M
+  middleware?: (getDefaultMiddleware: GetDefaultMiddleware<S>) => M
 
   /**
    * Whether to enable Redux DevTools integration. Defaults to `true`.
@@ -80,45 +75,24 @@ export interface ConfigureStoreOptions<
    * function (either directly or indirectly by passing an object as `reducer`),
    * this must be an object with the same shape as the reducer map keys.
    */
-  /*
-  Not 100% correct but the best approximation we can get:
-  - if S is a `CombinedState` applying a second `CombinedState` on it does not change anything.
-  - if it is not, there could be two cases:
-    - `ReducersMapObject<S, A>` is being passed in. In this case, we will call `combineReducers` on it and `CombinedState<S>` is correct
-    - `Reducer<S, A>` is being passed in. In this case, actually `CombinedState<S>` is wrong and `S` would be correct.
-    As we cannot distinguish between those two cases without adding another generic parameter,
-    we just make the pragmatic assumption that the latter almost never happens.
-  */
-  preloadedState?: PreloadedState<CombinedState<NoInfer<S>>>
+  // we infer here, and instead complain if the reducer doesn't match
+  preloadedState?: P
 
   /**
    * The store enhancers to apply. See Redux's `createStore()`.
    * All enhancers will be included before the DevTools Extension enhancer.
    * If you need to customize the order of enhancers, supply a callback
-   * function that will receive the original array (ie, `[applyMiddleware]`),
-   * and should return a new array (such as `[applyMiddleware, offline]`).
+   * function that will receive a `getDefaultEnhancers` function that returns a Tuple,
+   * and should return a Tuple of enhancers (such as `getDefaultEnhancers().concat(offline)`).
    * If you only need to add middleware, you can use the `middleware` parameter instead.
    */
-  enhancers?: E | ConfigureEnhancersCallback<E>
+  enhancers?: (getDefaultEnhancers: GetDefaultEnhancers<M>) => E
 }
 
-type Middlewares<S> = ReadonlyArray<Middleware<{}, S>>
+export type Middlewares<S> = ReadonlyArray<Middleware<{}, S>>
 
 type Enhancers = ReadonlyArray<StoreEnhancer>
 
-export interface ToolkitStore<
-  S = any,
-  A extends Action = AnyAction,
-  M extends Middlewares<S> = Middlewares<S>
-> extends Store<S, A> {
-  /**
-   * The `dispatch` method of your store, enhanced by all its middlewares.
-   *
-   * @inheritdoc
-   */
-  dispatch: ExtractDispatchExtensions<M> & Dispatch<A>
-}
-
 /**
  * A Redux store returned by `configureStore()`. Supports dispatching
  * side-effectful _thunks_ in addition to plain actions.
@@ -127,11 +101,9 @@ export interface ToolkitStore<
  */
 export type EnhancedStore<
   S = any,
-  A extends Action = AnyAction,
-  M extends Middlewares<S> = Middlewares<S>,
+  A extends Action = UnknownAction,
   E extends Enhancers = Enhancers
-> = ToolkitStore<S & ExtractStateExtensions<E>, A, M> &
-  ExtractStoreExtensions<E>
+> = ExtractStoreExtensions<E> & Store<S & ExtractStateExtensions<E>, A>
 
 /**
  * A friendly abstraction over the standard Redux `createStore()` function.
@@ -143,41 +115,50 @@ export type EnhancedStore<
  */
 export function configureStore<
   S = any,
-  A extends Action = AnyAction,
-  M extends Middlewares<S> = [ThunkMiddlewareFor<S>],
-  E extends Enhancers = [StoreEnhancer]
->(options: ConfigureStoreOptions<S, A, M, E>): EnhancedStore<S, A, M, E> {
-  const curriedGetDefaultMiddleware = curryGetDefaultMiddleware<S>()
+  A extends Action = UnknownAction,
+  M extends Tuple<Middlewares<S>> = Tuple<[ThunkMiddlewareFor<S>]>,
+  E extends Tuple<Enhancers> = Tuple<
+    [StoreEnhancer<{ dispatch: ExtractDispatchExtensions<M> }>, StoreEnhancer]
+  >,
+  P = S
+>(options: ConfigureStoreOptions<S, A, M, E, P>): EnhancedStore<S, A, E> {
+  const getDefaultMiddleware = buildGetDefaultMiddleware<S>()
 
   const {
     reducer = undefined,
-    middleware = curriedGetDefaultMiddleware(),
+    middleware,
     devTools = true,
     preloadedState = undefined,
     enhancers = undefined,
   } = options || {}
 
-  let rootReducer: Reducer<S, A>
+  let rootReducer: Reducer<S, A, P>
 
   if (typeof reducer === 'function') {
     rootReducer = reducer
   } else if (isPlainObject(reducer)) {
-    rootReducer = combineReducers(reducer) as unknown as Reducer<S, A>
+    rootReducer = combineReducers(reducer) as unknown as Reducer<S, A, P>
   } else {
     throw new Error(
-      '"reducer" is a required argument, and must be a function or an object of functions that can be passed to combineReducers'
+      '`reducer` is a required argument, and must be a function or an object of functions that can be passed to combineReducers'
     )
   }
 
-  let finalMiddleware = middleware
-  if (typeof finalMiddleware === 'function') {
-    finalMiddleware = finalMiddleware(curriedGetDefaultMiddleware)
+  if (!IS_PRODUCTION && middleware && typeof middleware !== 'function') {
+    throw new Error('`middleware` field must be a callback')
+  }
+
+  let finalMiddleware: Tuple<Middlewares<S>>
+  if (typeof middleware === 'function') {
+    finalMiddleware = middleware(getDefaultMiddleware)
 
     if (!IS_PRODUCTION && !Array.isArray(finalMiddleware)) {
       throw new Error(
         'when using a middleware builder function, an array of middleware must be returned'
       )
     }
+  } else {
+    finalMiddleware = getDefaultMiddleware()
   }
   if (
     !IS_PRODUCTION &&
@@ -188,8 +169,6 @@ export function configureStore<
     )
   }
 
-  const middlewareEnhancer: StoreEnhancer = applyMiddleware(...finalMiddleware)
-
   let finalCompose = compose
 
   if (devTools) {
@@ -200,16 +179,41 @@ export function configureStore<
     })
   }
 
-  const defaultEnhancers = new EnhancerArray(middlewareEnhancer)
-  let storeEnhancers: Enhancers = defaultEnhancers
+  const middlewareEnhancer = applyMiddleware(...finalMiddleware)
+
+  const getDefaultEnhancers = buildGetDefaultEnhancers<M>(middlewareEnhancer)
+
+  if (!IS_PRODUCTION && enhancers && typeof enhancers !== 'function') {
+    throw new Error('`enhancers` field must be a callback')
+  }
+
+  let storeEnhancers =
+    typeof enhancers === 'function'
+      ? enhancers(getDefaultEnhancers)
+      : getDefaultEnhancers()
 
-  if (Array.isArray(enhancers)) {
-    storeEnhancers = [middlewareEnhancer, ...enhancers]
-  } else if (typeof enhancers === 'function') {
-    storeEnhancers = enhancers(defaultEnhancers)
+  if (!IS_PRODUCTION && !Array.isArray(storeEnhancers)) {
+    throw new Error('`enhancers` callback must return an array')
+  }
+  if (
+    !IS_PRODUCTION &&
+    storeEnhancers.some((item: any) => typeof item !== 'function')
+  ) {
+    throw new Error(
+      'each enhancer provided to configureStore must be a function'
+    )
+  }
+  if (
+    !IS_PRODUCTION &&
+    finalMiddleware.length &&
+    !storeEnhancers.includes(middlewareEnhancer)
+  ) {
+    console.error(
+      'middlewares were provided, but middleware enhancer was not included in final enhancers - make sure to call `getDefaultEnhancers`'
+    )
   }
 
-  const composedEnhancer = finalCompose(...storeEnhancers) as StoreEnhancer<any>
+  const composedEnhancer: StoreEnhancer<any> = finalCompose(...storeEnhancers)
 
-  return createStore(rootReducer, preloadedState, composedEnhancer)
+  return createStore(rootReducer, preloadedState as P, composedEnhancer)
 }
diff --git a/packages/toolkit/src/createAction.ts b/packages/toolkit/src/createAction.ts
--- a/packages/toolkit/src/createAction.ts
+++ b/packages/toolkit/src/createAction.ts
@@ -1,4 +1,4 @@
-import type { Action } from 'redux'
+import { isAction } from 'redux'
 import type {
   IsUnknownOrNonInferrable,
   IfMaybeUndefined,
@@ -6,7 +6,6 @@ import type {
   IsAny,
 } from './tsHelpers'
 import { hasMatchFunction } from './tsHelpers'
-import isPlainObject from './isPlainObject'
 
 /**
  * An action with a string type and an associated payload. This is the
@@ -84,7 +83,7 @@ export type _ActionCreatorWithPreparedPayload<
  */
 export interface BaseActionCreator<P, T extends string, M = never, E = never> {
   type: T
-  match: (action: Action<unknown>) => action is PayloadAction<P, T, M, E>
+  match: (action: unknown) => action is PayloadAction<P, T, M, E>
 }
 
 /**
@@ -225,8 +224,7 @@ export type PayloadActionCreator<
  * A utility function to create an action creator for the given action type
  * string. The action creator accepts a single argument, which will be included
  * in the action object as a field called payload. The action creator function
- * will also have its toString() overridden so that it returns the action type,
- * allowing it to be used in reducer logic that is looking for that action type.
+ * will also have its toString() overridden so that it returns the action type.
  *
  * @param type The action type to use for created actions.
  * @param prepare (optional) a method that takes any number of arguments and returns { payload } or { payload, meta }.
@@ -242,8 +240,7 @@ export function createAction<P = void, T extends string = string>(
  * A utility function to create an action creator for the given action type
  * string. The action creator accepts a single argument, which will be included
  * in the action object as a field called payload. The action creator function
- * will also have its toString() overridden so that it returns the action type,
- * allowing it to be used in reducer logic that is looking for that action type.
+ * will also have its toString() overridden so that it returns the action type.
  *
  * @param type The action type to use for created actions.
  * @param prepare (optional) a method that takes any number of arguments and returns { payload } or { payload, meta }.
@@ -281,19 +278,12 @@ export function createAction(type: string, prepareAction?: Function): any {
 
   actionCreator.type = type
 
-  actionCreator.match = (action: Action<unknown>): action is PayloadAction =>
-    action.type === type
+  actionCreator.match = (action: unknown): action is PayloadAction =>
+    isAction(action) && action.type === type
 
   return actionCreator
 }
 
-/**
- * Returns true if value is a plain object with a `type` property.
- */
-export function isAction(action: unknown): action is Action<unknown> {
-  return isPlainObject(action) && 'type' in action
-}
-
 /**
  * Returns true if value is an RTK-like action creator, with a static type property and match method.
  */
@@ -317,33 +307,13 @@ export function isFSA(action: unknown): action is {
   error?: unknown
   meta?: unknown
 } {
-  return (
-    isAction(action) &&
-    typeof action.type === 'string' &&
-    Object.keys(action).every(isValidKey)
-  )
+  return isAction(action) && Object.keys(action).every(isValidKey)
 }
 
 function isValidKey(key: string) {
   return ['type', 'payload', 'error', 'meta'].indexOf(key) > -1
 }
 
-/**
- * Returns the action type of the actions created by the passed
- * `createAction()`-generated action creator (arbitrary action creators
- * are not supported).
- *
- * @param action The action creator whose action type to get.
- * @returns The action type used by the action creator.
- *
- * @public
- */
-export function getType<T extends string>(
-  actionCreator: PayloadActionCreator<any, T>
-): T {
-  return `${actionCreator}` as T
-}
-
 // helper types for more readable typings
 
 type IfPrepareActionMethodProvided<
diff --git a/packages/toolkit/src/createAsyncThunk.ts b/packages/toolkit/src/createAsyncThunk.ts
--- a/packages/toolkit/src/createAsyncThunk.ts
+++ b/packages/toolkit/src/createAsyncThunk.ts
@@ -1,12 +1,20 @@
-import type { Dispatch, AnyAction } from 'redux'
+import type { Dispatch, UnknownAction } from 'redux'
 import type {
   PayloadAction,
   ActionCreatorWithPreparedPayload,
 } from './createAction'
 import { createAction } from './createAction'
 import type { ThunkDispatch } from 'redux-thunk'
-import type { FallbackIfUnknown, Id, IsAny, IsUnknown } from './tsHelpers'
+import type {
+  ActionFromMatcher,
+  FallbackIfUnknown,
+  Id,
+  IsAny,
+  IsUnknown,
+  TypeGuard,
+} from './tsHelpers'
 import { nanoid } from './nanoid'
+import { isAnyOf } from './matchers'
 
 // @ts-ignore we need the import of these types due to a bundling issue.
 type _Keep = PayloadAction | ActionCreatorWithPreparedPayload<any, unknown>
@@ -105,7 +113,7 @@ export const miniSerializeError = (value: any): SerializedError => {
   return { message: String(value) }
 }
 
-type AsyncThunkConfig = {
+export type AsyncThunkConfig = {
   state?: unknown
   dispatch?: Dispatch
   extra?: unknown
@@ -132,10 +140,14 @@ type GetDispatch<ThunkApiConfig> = ThunkApiConfig extends {
       ThunkDispatch<
         GetState<ThunkApiConfig>,
         GetExtra<ThunkApiConfig>,
-        AnyAction
+        UnknownAction
       >
     >
-  : ThunkDispatch<GetState<ThunkApiConfig>, GetExtra<ThunkApiConfig>, AnyAction>
+  : ThunkDispatch<
+      GetState<ThunkApiConfig>,
+      GetExtra<ThunkApiConfig>,
+      UnknownAction
+    >
 
 export type GetThunkAPI<ThunkApiConfig> = BaseThunkAPI<
   GetState<ThunkApiConfig>,
@@ -411,10 +423,17 @@ export type AsyncThunk<
     ThunkArg,
     ThunkApiConfig
   >
+  // matchSettled?
+  settled: (
+    action: any
+  ) => action is ReturnType<
+    | AsyncThunkRejectedActionCreator<ThunkArg, ThunkApiConfig>
+    | AsyncThunkFulfilledActionCreator<Returned, ThunkArg, ThunkApiConfig>
+  >
   typePrefix: string
 }
 
-type OverrideThunkApiConfigs<OldConfig, NewConfig> = Id<
+export type OverrideThunkApiConfigs<OldConfig, NewConfig> = Id<
   NewConfig & Omit<OldConfig, keyof NewConfig>
 >
 
@@ -468,7 +487,7 @@ type CreateAsyncThunk<CurriedThunkApiConfig extends AsyncThunkConfig> = {
   >
 }
 
-export const createAsyncThunk = (() => {
+export const createAsyncThunk = /* @__PURE__ */ (() => {
   function createAsyncThunk<
     Returned,
     ThunkArg,
@@ -549,36 +568,6 @@ export const createAsyncThunk = (() => {
         })
       )
 
-    let displayedWarning = false
-
-    const AC =
-      typeof AbortController !== 'undefined'
-        ? AbortController
-        : class implements AbortController {
-            signal = {
-              aborted: false,
-              addEventListener() {},
-              dispatchEvent() {
-                return false
-              },
-              onabort() {},
-              removeEventListener() {},
-              reason: undefined,
-              throwIfAborted() {},
-            }
-            abort() {
-              if (process.env.NODE_ENV !== 'production') {
-                if (!displayedWarning) {
-                  displayedWarning = true
-                  console.info(
-                    `This platform does not implement AbortController. 
-If you want to use the AbortController to react to \`abort\` events, please consider importing a polyfill like 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'.`
-                  )
-                }
-              }
-            }
-          }
-
     function actionCreator(
       arg: ThunkArg
     ): AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> {
@@ -587,10 +576,9 @@ If you want to use the AbortController to react to \`abort\` events, please cons
           ? options.idGenerator(arg)
           : nanoid()
 
-        const abortController = new AC()
+        const abortController = new AbortController()
         let abortReason: string | undefined
 
-        let started = false
         function abort(reason?: string) {
           abortReason = reason
           abortController.abort()
@@ -611,7 +599,6 @@ If you want to use the AbortController to react to \`abort\` events, please cons
                 message: 'Aborted due to condition callback returning false.',
               }
             }
-            started = true
 
             const abortedPromise = new Promise<never>((_, reject) =>
               abortController.signal.addEventListener('abort', () =>
@@ -629,7 +616,7 @@ If you want to use the AbortController to react to \`abort\` events, please cons
                   { requestId, arg },
                   { getState, extra }
                 )
-              )
+              ) as any
             )
             finalAction = await Promise.race([
               abortedPromise,
@@ -679,7 +666,7 @@ If you want to use the AbortController to react to \`abort\` events, please cons
             (finalAction as any).meta.condition
 
           if (!skipDispatch) {
-            dispatch(finalAction)
+            dispatch(finalAction as any)
           }
           return finalAction
         })()
@@ -704,6 +691,7 @@ If you want to use the AbortController to react to \`abort\` events, please cons
         pending,
         rejected,
         fulfilled,
+        settled: isAnyOf(rejected, fulfilled),
         typePrefix,
       }
     )
diff --git a/packages/toolkit/src/createDraftSafeSelector.ts b/packages/toolkit/src/createDraftSafeSelector.ts
--- a/packages/toolkit/src/createDraftSafeSelector.ts
+++ b/packages/toolkit/src/createDraftSafeSelector.ts
@@ -1,5 +1,18 @@
 import { current, isDraft } from 'immer'
-import { createSelector } from 'reselect'
+import { createSelectorCreator, weakMapMemoize } from 'reselect'
+
+export const createDraftSafeSelectorCreator: typeof createSelectorCreator = (
+  ...args: unknown[]
+) => {
+  const createSelector = (createSelectorCreator as any)(...args)
+  return (...args: unknown[]) => {
+    const selector = createSelector(...args)
+    const wrappedSelector = (value: unknown, ...rest: unknown[]) =>
+      selector(isDraft(value) ? current(value) : value, ...rest)
+    Object.assign(wrappedSelector, selector)
+    return wrappedSelector as any
+  }
+}
 
 /**
  * "Draft-Safe" version of `reselect`'s `createSelector`:
@@ -8,11 +21,5 @@ import { createSelector } from 'reselect'
  * that might be possibly outdated if the draft has been modified since.
  * @public
  */
-export const createDraftSafeSelector: typeof createSelector = (
-  ...args: unknown[]
-) => {
-  const selector = (createSelector as any)(...args)
-  const wrappedSelector = (value: unknown, ...rest: unknown[]) =>
-    selector(isDraft(value) ? current(value) : value, ...rest)
-  return wrappedSelector as any
-}
+export const createDraftSafeSelector =
+  createDraftSafeSelectorCreator(weakMapMemoize)
diff --git a/packages/toolkit/src/createReducer.ts b/packages/toolkit/src/createReducer.ts
--- a/packages/toolkit/src/createReducer.ts
+++ b/packages/toolkit/src/createReducer.ts
@@ -1,9 +1,9 @@
 import type { Draft } from 'immer'
-import createNextState, { isDraft, isDraftable } from 'immer'
-import type { AnyAction, Action, Reducer } from 'redux'
+import { produce as createNextState, isDraft, isDraftable } from 'immer'
+import type { Action, Reducer, UnknownAction } from 'redux'
 import type { ActionReducerMapBuilder } from './mapBuilders'
 import { executeReducerBuilderCallback } from './mapBuilders'
-import type { NoInfer } from './tsHelpers'
+import type { NoInfer, TypeGuard } from './tsHelpers'
 import { freezeDraftable } from './utils'
 
 /**
@@ -16,15 +16,8 @@ import { freezeDraftable } from './utils'
  */
 export type Actions<T extends keyof any = string> = Record<T, Action>
 
-/**
- * @deprecated use `TypeGuard` instead
- */
-export interface ActionMatcher<A extends AnyAction> {
-  (action: AnyAction): action is A
-}
-
-export type ActionMatcherDescription<S, A extends AnyAction> = {
-  matcher: ActionMatcher<A>
+export type ActionMatcherDescription<S, A extends Action> = {
+  matcher: TypeGuard<A>
   reducer: CaseReducer<S, NoInfer<A>>
 }
 
@@ -52,7 +45,7 @@ export type ActionMatcherDescriptionCollection<S> = Array<
  *
  * @public
  */
-export type CaseReducer<S = any, A extends Action = AnyAction> = (
+export type CaseReducer<S = any, A extends Action = UnknownAction> = (
   state: Draft<S>,
   action: A
 ) => NoInfer<S> | void | Draft<NoInfer<S>>
@@ -80,8 +73,6 @@ export type ReducerWithInitialState<S extends NotFunction<any>> = Reducer<S> & {
   getInitialState: () => S
 }
 
-let hasWarnedAboutObjectNotation = false
-
 /**
  * A utility function that allows defining a reducer as a mapping from action
  * type to *case reducer* functions that handle these action types. The
@@ -96,7 +87,7 @@ let hasWarnedAboutObjectNotation = false
  * convenience and immutability.
  *
  * @overloadSummary
- * This overload accepts a callback function that receives a `builder` object as its argument.
+ * This function accepts a callback that receives a `builder` object as its argument.
  * That builder provides `addCase`, `addMatcher` and `addDefaultCase` functions that may be
  * called to define what actions this reducer will handle.
  *
@@ -108,7 +99,7 @@ let hasWarnedAboutObjectNotation = false
 import {
   createAction,
   createReducer,
-  AnyAction,
+  UnknownAction,
   PayloadAction,
 } from "@reduxjs/toolkit";
 
@@ -116,7 +107,7 @@ const increment = createAction<number>("increment");
 const decrement = createAction<number>("decrement");
 
 function isActionWithNumberPayload(
-  action: AnyAction
+  action: UnknownAction
 ): action is PayloadAction<number> {
   return typeof action.payload === "number";
 }
@@ -148,94 +139,18 @@ const reducer = createReducer(
  */
 export function createReducer<S extends NotFunction<any>>(
   initialState: S | (() => S),
-  builderCallback: (builder: ActionReducerMapBuilder<S>) => void
-): ReducerWithInitialState<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
- * reducer's initial state is passed as the first argument.
- *
- * The body of every case reducer is implicitly wrapped with a call to
- * `produce()` from the [immer](https://github.com/mweststrate/immer) library.
- * This means that rather than returning a new state object, you can also
- * mutate the passed-in state object directly; these mutations will then be
- * automatically and efficiently translated into copies, giving you both
- * convenience and immutability.
- * 
- * @overloadSummary
- * This overload accepts an object where the keys are string action types, and the values
- * are case reducer functions to handle those action types.
- *
- * @param initialState - `State | (() => State)`: The initial state that should be used when the reducer is called the first time. This may also be a "lazy initializer" function, which should return an initial state value when called. This will be used whenever the reducer is called with `undefined` as its state value, and is primarily useful for cases like reading initial state from `localStorage`.
- * @param actionsMap - An object mapping from action types to _case reducers_, each of which handles one specific action type.
- * @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.
- *
- * @example
-```js
-const counterReducer = createReducer(0, {
-  increment: (state, action) => state + action.payload,
-  decrement: (state, action) => state - action.payload
-})
-
-// Alternately, use a "lazy initializer" to provide the initial state
-// (works with either form of createReducer)
-const initialState = () => 0
-const counterReducer = createReducer(initialState, {
-  increment: (state, action) => state + action.payload,
-  decrement: (state, action) => state - action.payload
-})
-```
- 
- * Action creators that were generated using [`createAction`](./createAction) may be used directly as the keys here, using computed property syntax:
-
-```js
-const increment = createAction('increment')
-const decrement = createAction('decrement')
-
-const counterReducer = createReducer(0, {
-  [increment]: (state, action) => state + action.payload,
-  [decrement.type]: (state, action) => state - action.payload
-})
-```
- * @public
- */
-export function createReducer<
-  S extends NotFunction<any>,
-  CR extends CaseReducers<S, any> = CaseReducers<S, any>
->(
-  initialState: S | (() => S),
-  actionsMap: CR,
-  actionMatchers?: ActionMatcherDescriptionCollection<S>,
-  defaultCaseReducer?: CaseReducer<S>
-): ReducerWithInitialState<S>
-
-export function createReducer<S extends NotFunction<any>>(
-  initialState: S | (() => S),
-  mapOrBuilderCallback:
-    | CaseReducers<S, any>
-    | ((builder: ActionReducerMapBuilder<S>) => void),
-  actionMatchers: ReadonlyActionMatcherDescriptionCollection<S> = [],
-  defaultCaseReducer?: CaseReducer<S>
+  mapOrBuilderCallback: (builder: ActionReducerMapBuilder<S>) => void
 ): ReducerWithInitialState<S> {
   if (process.env.NODE_ENV !== 'production') {
     if (typeof mapOrBuilderCallback === 'object') {
-      if (!hasWarnedAboutObjectNotation) {
-        hasWarnedAboutObjectNotation = true
-        console.warn(
-          "The object notation for `createReducer` is deprecated, and will be removed in RTK 2.0. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createReducer"
-        )
-      }
+      throw new Error(
+        "The object notation for `createReducer` has been removed. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createReducer"
+      )
     }
   }
 
   let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
-    typeof mapOrBuilderCallback === 'function'
-      ? executeReducerBuilderCallback(mapOrBuilderCallback)
-      : [mapOrBuilderCallback, actionMatchers, defaultCaseReducer]
+    executeReducerBuilderCallback(mapOrBuilderCallback)
 
   // Ensure the initial state gets frozen either way (if draftable)
   let getInitialState: () => S
diff --git a/packages/toolkit/src/createSlice.ts b/packages/toolkit/src/createSlice.ts
--- a/packages/toolkit/src/createSlice.ts
+++ b/packages/toolkit/src/createSlice.ts
@@ -1,5 +1,5 @@
-import type { AnyAction, Reducer } from 'redux'
-import { createNextState } from '.'
+import type { Action, UnknownAction, Reducer } from 'redux'
+import type { Selector } from 'reselect'
 import type {
   ActionCreatorWithoutPayload,
   PayloadAction,
@@ -9,26 +9,36 @@ import type {
 } from './createAction'
 import { createAction } from './createAction'
 import type {
+  ActionMatcherDescriptionCollection,
   CaseReducer,
-  CaseReducers,
   ReducerWithInitialState,
 } from './createReducer'
-import { createReducer, NotFunction } from './createReducer'
-import type { ActionReducerMapBuilder } from './mapBuilders'
+import { createReducer } from './createReducer'
+import type { ActionReducerMapBuilder, TypedActionCreator } from './mapBuilders'
 import { executeReducerBuilderCallback } from './mapBuilders'
-import type { NoInfer } from './tsHelpers'
-import { freezeDraftable } from './utils'
+import type { Id, Tail, TypeGuard } from './tsHelpers'
+import type { InjectConfig } from './combineSlices'
+import type {
+  AsyncThunk,
+  AsyncThunkConfig,
+  AsyncThunkOptions,
+  AsyncThunkPayloadCreator,
+  OverrideThunkApiConfigs,
+} from './createAsyncThunk'
+import { createAsyncThunk as _createAsyncThunk } from './createAsyncThunk'
+import { emplace } from './utils'
 
-let hasWarnedAboutObjectNotation = false
+const asyncThunkSymbol = Symbol.for('rtk-slice-createasyncthunk')
+// type is annotated because it's too long to infer
+export const asyncThunkCreator: {
+  [asyncThunkSymbol]: typeof _createAsyncThunk
+} = {
+  [asyncThunkSymbol]: _createAsyncThunk,
+}
 
-/**
- * An action creator attached to a slice.
- *
- * @deprecated please use PayloadActionCreator directly
- *
- * @public
- */
-export type SliceActionCreator<P> = PayloadActionCreator<P>
+interface InjectIntoConfig<NewReducerPath extends string> extends InjectConfig {
+  reducerPath?: NewReducerPath
+}
 
 /**
  * The return value of `createSlice`
@@ -38,13 +48,20 @@ export type SliceActionCreator<P> = PayloadActionCreator<P>
 export interface Slice<
   State = any,
   CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
-  Name extends string = string
+  Name extends string = string,
+  ReducerPath extends string = Name,
+  Selectors extends SliceSelectors<State> = SliceSelectors<State>
 > {
   /**
    * The slice name.
    */
   name: Name
 
+  /**
+   *  The slice reducer path.
+   */
+  reducerPath: ReducerPath
+
   /**
    * The slice's reducer.
    */
@@ -67,6 +84,97 @@ export interface Slice<
    * If a lazy state initializer was provided, it will be called and a fresh value returned.
    */
   getInitialState: () => State
+
+  /**
+   * Get localised slice selectors (expects to be called with *just* the slice's state as the first parameter)
+   */
+  getSelectors(this: this): Id<SliceDefinedSelectors<State, Selectors, State>>
+
+  /**
+   * Get globalised slice selectors (`selectState` callback is expected to receive first parameter and return slice state)
+   */
+  getSelectors<RootState>(
+    this: this,
+    selectState: (this: this, rootState: RootState) => State
+  ): Id<SliceDefinedSelectors<State, Selectors, RootState>>
+
+  /**
+   * Selectors that assume the slice's state is `rootState[slice.reducerPath]` (which is usually the case)
+   *
+   * Equivalent to `slice.getSelectors((state: RootState) => state[slice.reducerPath])`.
+   */
+  selectors: Id<
+    SliceDefinedSelectors<State, Selectors, { [K in ReducerPath]: State }>
+  >
+
+  /**
+   * Inject slice into provided reducer (return value from `combineSlices`), and return injected slice.
+   */
+  injectInto<NewReducerPath extends string = ReducerPath>(
+    this: this,
+    injectable: {
+      inject: (
+        slice: { reducerPath: string; reducer: Reducer },
+        config?: InjectConfig
+      ) => void
+    },
+    config?: InjectIntoConfig<NewReducerPath>
+  ): InjectedSlice<State, CaseReducers, Name, NewReducerPath, Selectors>
+
+  /**
+   * Select the slice state, using the slice's current reducerPath.
+   *
+   * Will throw an error if slice is not found.
+   */
+  selectSlice(this: this, state: { [K in ReducerPath]: State }): State
+}
+
+/**
+ * A slice after being called with `injectInto(reducer)`.
+ *
+ * Selectors can now be called with an `undefined` value, in which case they use the slice's initial state.
+ */
+interface InjectedSlice<
+  State = any,
+  CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
+  Name extends string = string,
+  ReducerPath extends string = Name,
+  Selectors extends SliceSelectors<State> = SliceSelectors<State>
+> extends Omit<
+    Slice<State, CaseReducers, Name, ReducerPath, Selectors>,
+    'getSelectors' | 'selectors'
+  > {
+  /**
+   * Get localised slice selectors (expects to be called with *just* the slice's state as the first parameter)
+   */
+  getSelectors(): Id<SliceDefinedSelectors<State, Selectors, State | undefined>>
+
+  /**
+   * Get globalised slice selectors (`selectState` callback is expected to receive first parameter and return slice state)
+   */
+  getSelectors<RootState>(
+    selectState: (this: this, rootState: RootState) => State | undefined
+  ): Id<SliceDefinedSelectors<State, Selectors, RootState>>
+
+  /**
+   * Selectors that assume the slice's state is `rootState[slice.name]` (which is usually the case)
+   *
+   * Equivalent to `slice.getSelectors((state: RootState) => state[slice.name])`.
+   */
+  selectors: Id<
+    SliceDefinedSelectors<
+      State,
+      Selectors,
+      { [K in ReducerPath]?: State | undefined }
+    >
+  >
+
+  /**
+   * Select the slice state, using the slice's current reducerPath.
+   *
+   * Returns initial state if slice is not found.
+   */
+  selectSlice(state: { [K in ReducerPath]?: State | undefined }): State
 }
 
 /**
@@ -77,13 +185,20 @@ export interface Slice<
 export interface CreateSliceOptions<
   State = any,
   CR extends SliceCaseReducers<State> = SliceCaseReducers<State>,
-  Name extends string = string
+  Name extends string = string,
+  ReducerPath extends string = Name,
+  Selectors extends SliceSelectors<State> = SliceSelectors<State>
 > {
   /**
    * The slice's name. Used to namespace the generated action types.
    */
   name: Name
 
+  /**
+   * The slice's reducer path. Used when injecting into a combined slice reducer.
+   */
+  reducerPath?: ReducerPath
+
   /**
    * The initial state that should be used when the reducer is called the first time. This may also be a "lazy initializer" function, which should return an initial state value when called. This will be used whenever the reducer is called with `undefined` as its state value, and is primarily useful for cases like reading initial state from `localStorage`.
    */
@@ -94,19 +209,18 @@ export interface CreateSliceOptions<
    * functions. For every action type, a matching action creator will be
    * generated using `createAction()`.
    */
-  reducers: ValidateSliceCaseReducers<State, CR>
+  reducers:
+    | ValidateSliceCaseReducers<State, CR>
+    | ((creators: ReducerCreators<State>) => CR)
 
   /**
    * A callback that receives a *builder* object to define
    * case reducers via calls to `builder.addCase(actionCreatorOrType, reducer)`.
    * 
-   * Alternatively, a mapping from action types to action-type-specific *case reducer*
-   * functions. These reducers should have existing action types used
-   * as the keys, and action creators will _not_ be generated.
    * 
    * @example
 ```ts
-import { createAction, createSlice, Action, AnyAction } from '@reduxjs/toolkit'
+import { createAction, createSlice, Action } from '@reduxjs/toolkit'
 const incrementBy = createAction<number>('incrementBy')
 const decrement = createAction('decrement')
 
@@ -114,7 +228,7 @@ interface RejectedAction extends Action {
   error: Error
 }
 
-function isRejectedAction(action: AnyAction): action is RejectedAction {
+function isRejectedAction(action: Action): action is RejectedAction {
   return action.type.endsWith('rejected')
 }
 
@@ -141,11 +255,30 @@ createSlice({
 })
 ```
    */
-  extraReducers?:
-    | CaseReducers<NoInfer<State>, any>
-    | ((builder: ActionReducerMapBuilder<NoInfer<State>>) => void)
+  extraReducers?: (builder: ActionReducerMapBuilder<State>) => void
+
+  /**
+   * A map of selectors that receive the slice's state and any additional arguments, and return a result.
+   */
+  selectors?: Selectors
+}
+
+export enum ReducerType {
+  reducer = 'reducer',
+  reducerWithPrepare = 'reducerWithPrepare',
+  asyncThunk = 'asyncThunk',
 }
 
+interface ReducerDefinition<T extends ReducerType = ReducerType> {
+  _reducerDefinitionType: T
+}
+
+export interface CaseReducerDefinition<
+  S = any,
+  A extends Action = UnknownAction
+> extends CaseReducer<S, A>,
+    ReducerDefinition<ReducerType.reducer> {}
+
 /**
  * A CaseReducer with a `prepare` method.
  *
@@ -156,15 +289,162 @@ export type CaseReducerWithPrepare<State, Action extends PayloadAction> = {
   prepare: PrepareAction<Action['payload']>
 }
 
+export interface CaseReducerWithPrepareDefinition<
+  State,
+  Action extends PayloadAction
+> extends CaseReducerWithPrepare<State, Action>,
+    ReducerDefinition<ReducerType.reducerWithPrepare> {}
+
+export interface AsyncThunkSliceReducerConfig<
+  State,
+  ThunkArg extends any,
+  Returned = unknown,
+  ThunkApiConfig extends AsyncThunkConfig = {}
+> {
+  pending?: CaseReducer<
+    State,
+    ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['pending']>
+  >
+  rejected?: CaseReducer<
+    State,
+    ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['rejected']>
+  >
+  fulfilled?: CaseReducer<
+    State,
+    ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['fulfilled']>
+  >
+  settled?: CaseReducer<
+    State,
+    ReturnType<
+      AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['rejected' | 'fulfilled']
+    >
+  >
+  options?: AsyncThunkOptions<ThunkArg, ThunkApiConfig>
+}
+
+export interface AsyncThunkSliceReducerDefinition<
+  State,
+  ThunkArg extends any,
+  Returned = unknown,
+  ThunkApiConfig extends AsyncThunkConfig = {}
+> extends AsyncThunkSliceReducerConfig<
+      State,
+      ThunkArg,
+      Returned,
+      ThunkApiConfig
+    >,
+    ReducerDefinition<ReducerType.asyncThunk> {
+  payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig>
+}
+
+/**
+ * Providing these as part of the config would cause circular types, so we disallow passing them
+ */
+type PreventCircular<ThunkApiConfig> = {
+  [K in keyof ThunkApiConfig]: K extends 'state' | 'dispatch'
+    ? never
+    : ThunkApiConfig[K]
+}
+
+interface AsyncThunkCreator<
+  State,
+  CurriedThunkApiConfig extends PreventCircular<AsyncThunkConfig> = PreventCircular<AsyncThunkConfig>
+> {
+  <ThunkArg extends any, Returned = unknown>(
+    payloadCreator: AsyncThunkPayloadCreator<
+      Returned,
+      ThunkArg,
+      CurriedThunkApiConfig
+    >,
+    config?: AsyncThunkSliceReducerConfig<
+      State,
+      ThunkArg,
+      Returned,
+      CurriedThunkApiConfig
+    >
+  ): AsyncThunkSliceReducerDefinition<
+    State,
+    ThunkArg,
+    Returned,
+    CurriedThunkApiConfig
+  >
+  <
+    ThunkArg extends any,
+    Returned = unknown,
+    ThunkApiConfig extends PreventCircular<AsyncThunkConfig> = {}
+  >(
+    payloadCreator: AsyncThunkPayloadCreator<
+      Returned,
+      ThunkArg,
+      ThunkApiConfig
+    >,
+    config?: AsyncThunkSliceReducerConfig<
+      State,
+      ThunkArg,
+      Returned,
+      ThunkApiConfig
+    >
+  ): AsyncThunkSliceReducerDefinition<State, ThunkArg, Returned, ThunkApiConfig>
+  withTypes<
+    ThunkApiConfig extends PreventCircular<AsyncThunkConfig>
+  >(): AsyncThunkCreator<
+    State,
+    OverrideThunkApiConfigs<CurriedThunkApiConfig, ThunkApiConfig>
+  >
+}
+
+export interface ReducerCreators<State> {
+  reducer(
+    caseReducer: CaseReducer<State, PayloadAction>
+  ): CaseReducerDefinition<State, PayloadAction>
+  reducer<Payload>(
+    caseReducer: CaseReducer<State, PayloadAction<Payload>>
+  ): CaseReducerDefinition<State, PayloadAction<Payload>>
+
+  asyncThunk: AsyncThunkCreator<State>
+
+  preparedReducer<Prepare extends PrepareAction<any>>(
+    prepare: Prepare,
+    reducer: CaseReducer<
+      State,
+      ReturnType<_ActionCreatorWithPreparedPayload<Prepare>>
+    >
+  ): {
+    _reducerDefinitionType: ReducerType.reducerWithPrepare
+    prepare: Prepare
+    reducer: CaseReducer<
+      State,
+      ReturnType<_ActionCreatorWithPreparedPayload<Prepare>>
+    >
+  }
+}
+
 /**
  * The type describing a slice's `reducers` option.
  *
  * @public
  */
-export type SliceCaseReducers<State> = {
-  [K: string]:
-    | CaseReducer<State, PayloadAction<any>>
-    | CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>
+export type SliceCaseReducers<State> =
+  | Record<
+      string,
+      | CaseReducerDefinition<State, PayloadAction<any>>
+      | CaseReducerWithPrepareDefinition<
+          State,
+          PayloadAction<any, string, any, any>
+        >
+      | AsyncThunkSliceReducerDefinition<State, any, any, any>
+    >
+  | Record<
+      string,
+      | CaseReducer<State, PayloadAction<any>>
+      | CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>
+    >
+
+/**
+ * The type describing a slice's `selectors` option.
+ */
+export type SliceSelectors<State> = {
+  [K: string]: (sliceState: State, ...args: any[]) => any
 }
 
 type SliceActionType<
@@ -181,15 +461,29 @@ export type CaseReducerActions<
   CaseReducers extends SliceCaseReducers<any>,
   SliceName extends string
 > = {
-  [Type in keyof CaseReducers]: CaseReducers[Type] extends { prepare: any }
-    ? ActionCreatorForCaseReducerWithPrepare<
-        CaseReducers[Type],
-        SliceActionType<SliceName, Type>
-      >
-    : ActionCreatorForCaseReducer<
-        CaseReducers[Type],
-        SliceActionType<SliceName, Type>
-      >
+  [Type in keyof CaseReducers]: CaseReducers[Type] extends infer Definition
+    ? Definition extends { prepare: any }
+      ? ActionCreatorForCaseReducerWithPrepare<
+          Definition,
+          SliceActionType<SliceName, Type>
+        >
+      : Definition extends AsyncThunkSliceReducerDefinition<
+          any,
+          infer ThunkArg,
+          infer Returned,
+          infer ThunkApiConfig
+        >
+      ? AsyncThunk<Returned, ThunkArg, ThunkApiConfig>
+      : Definition extends { reducer: any }
+      ? ActionCreatorForCaseReducer<
+          Definition['reducer'],
+          SliceActionType<SliceName, Type>
+        >
+      : ActionCreatorForCaseReducer<
+          Definition,
+          SliceActionType<SliceName, Type>
+        >
+    : never
 }
 
 /**
@@ -223,11 +517,44 @@ type ActionCreatorForCaseReducer<CR, Type extends string> = CR extends (
  * @internal
  */
 type SliceDefinedCaseReducers<CaseReducers extends SliceCaseReducers<any>> = {
-  [Type in keyof CaseReducers]: CaseReducers[Type] extends {
-    reducer: infer Reducer
-  }
-    ? Reducer
-    : CaseReducers[Type]
+  [Type in keyof CaseReducers]: CaseReducers[Type] extends infer Definition
+    ? Definition extends AsyncThunkSliceReducerDefinition<any, any, any, any>
+      ? Id<
+          Pick<
+            Required<Definition>,
+            'fulfilled' | 'rejected' | 'pending' | 'settled'
+          >
+        >
+      : Definition extends {
+          reducer: infer Reducer
+        }
+      ? Reducer
+      : Definition
+    : never
+}
+
+type RemappedSelector<S extends Selector, NewState> = S extends Selector<
+  any,
+  infer R,
+  infer P
+>
+  ? Selector<NewState, R, P> & { unwrapped: S }
+  : never
+
+/**
+ * Extracts the final selector type from the `selectors` object.
+ *
+ * Removes the `string` index signature from the default value.
+ */
+type SliceDefinedSelectors<
+  State,
+  Selectors extends SliceSelectors<State>,
+  RootState
+> = {
+  [K in keyof Selectors as string extends K ? never : K]: RemappedSelector<
+    Selectors[K],
+    RootState
+  >
 }
 
 /**
@@ -260,123 +587,484 @@ function getType(slice: string, actionKey: string): string {
   return `${slice}/${actionKey}`
 }
 
-/**
- * A function that accepts an initial state, an object full of reducer
- * functions, and a "slice name", and automatically generates
- * action creators and action types that correspond to the
- * reducers and state.
- *
- * The `reducer` argument is passed to `createReducer()`.
- *
- * @public
- */
-export function createSlice<
-  State,
-  CaseReducers extends SliceCaseReducers<State>,
-  Name extends string = string
->(
-  options: CreateSliceOptions<State, CaseReducers, Name>
-): Slice<State, CaseReducers, Name> {
-  const { name } = options
-  if (!name) {
-    throw new Error('`name` is a required option for createSlice')
+interface BuildCreateSliceConfig {
+  creators?: {
+    asyncThunk?: typeof asyncThunkCreator
   }
+}
 
-  if (
-    typeof process !== 'undefined' &&
-    process.env.NODE_ENV === 'development'
-  ) {
-    if (options.initialState === undefined) {
-      console.error(
-        'You must provide an `initialState` value that is not `undefined`. You may have misspelled `initialState`'
-      )
+export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
+  const cAT = creators?.asyncThunk?.[asyncThunkSymbol]
+  return function createSlice<
+    State,
+    CaseReducers extends SliceCaseReducers<State>,
+    Name extends string,
+    Selectors extends SliceSelectors<State>,
+    ReducerPath extends string = Name
+  >(
+    options: CreateSliceOptions<
+      State,
+      CaseReducers,
+      Name,
+      ReducerPath,
+      Selectors
+    >
+  ): Slice<State, CaseReducers, Name, ReducerPath, Selectors> {
+    const { name, reducerPath = name as unknown as ReducerPath } = options
+    if (!name) {
+      throw new Error('`name` is a required option for createSlice')
     }
-  }
-
-  const initialState =
-    typeof options.initialState == 'function'
-      ? options.initialState
-      : freezeDraftable(options.initialState)
 
-  const reducers = options.reducers || {}
-
-  const reducerNames = Object.keys(reducers)
+    if (
+      typeof process !== 'undefined' &&
+      process.env.NODE_ENV === 'development'
+    ) {
+      if (options.initialState === undefined) {
+        console.error(
+          'You must provide an `initialState` value that is not `undefined`. You may have misspelled `initialState`'
+        )
+      }
+    }
 
-  const sliceCaseReducersByName: Record<string, CaseReducer> = {}
-  const sliceCaseReducersByType: Record<string, CaseReducer> = {}
-  const actionCreators: Record<string, Function> = {}
+    const reducers =
+      (typeof options.reducers === 'function'
+        ? options.reducers(buildReducerCreators<State>())
+        : options.reducers) || {}
 
-  reducerNames.forEach((reducerName) => {
-    const maybeReducerWithPrepare = reducers[reducerName]
-    const type = getType(name, reducerName)
+    const reducerNames = Object.keys(reducers)
 
-    let caseReducer: CaseReducer<State, any>
-    let prepareCallback: PrepareAction<any> | undefined
+    const context: ReducerHandlingContext<State> = {
+      sliceCaseReducersByName: {},
+      sliceCaseReducersByType: {},
+      actionCreators: {},
+      sliceMatchers: [],
+    }
 
-    if ('reducer' in maybeReducerWithPrepare) {
-      caseReducer = maybeReducerWithPrepare.reducer
-      prepareCallback = maybeReducerWithPrepare.prepare
-    } else {
-      caseReducer = maybeReducerWithPrepare
+    const contextMethods: ReducerHandlingContextMethods<State> = {
+      addCase(
+        typeOrActionCreator: string | TypedActionCreator<any>,
+        reducer: CaseReducer<State>
+      ) {
+        const type =
+          typeof typeOrActionCreator === 'string'
+            ? typeOrActionCreator
+            : typeOrActionCreator.type
+        if (!type) {
+          throw new Error(
+            '`context.addCase` cannot be called with an empty action type'
+          )
+        }
+        if (type in context.sliceCaseReducersByType) {
+          throw new Error(
+            '`context.addCase` cannot be called with two reducers for the same action type: ' +
+              type
+          )
+        }
+        context.sliceCaseReducersByType[type] = reducer
+        return contextMethods
+      },
+      addMatcher(matcher, reducer) {
+        context.sliceMatchers.push({ matcher, reducer })
+        return contextMethods
+      },
+      exposeAction(name, actionCreator) {
+        context.actionCreators[name] = actionCreator
+        return contextMethods
+      },
+      exposeCaseReducer(name, reducer) {
+        context.sliceCaseReducersByName[name] = reducer
+        return contextMethods
+      },
     }
 
-    sliceCaseReducersByName[reducerName] = caseReducer
-    sliceCaseReducersByType[type] = caseReducer
-    actionCreators[reducerName] = prepareCallback
-      ? createAction(type, prepareCallback)
-      : createAction(type)
-  })
+    reducerNames.forEach((reducerName) => {
+      const reducerDefinition = reducers[reducerName]
+      const reducerDetails: ReducerDetails = {
+        reducerName,
+        type: getType(name, reducerName),
+        createNotation: typeof options.reducers === 'function',
+      }
+      if (isAsyncThunkSliceReducerDefinition<State>(reducerDefinition)) {
+        handleThunkCaseReducerDefinition(
+          reducerDetails,
+          reducerDefinition,
+          contextMethods,
+          cAT
+        )
+      } else {
+        handleNormalReducerDefinition<State>(
+          reducerDetails,
+          reducerDefinition,
+          contextMethods
+        )
+      }
+    })
 
-  function buildReducer() {
-    if (process.env.NODE_ENV !== 'production') {
-      if (typeof options.extraReducers === 'object') {
-        if (!hasWarnedAboutObjectNotation) {
-          hasWarnedAboutObjectNotation = true
-          console.warn(
-            "The object notation for `createSlice.extraReducers` is deprecated, and will be removed in RTK 2.0. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createSlice"
+    function buildReducer() {
+      if (process.env.NODE_ENV !== 'production') {
+        if (typeof options.extraReducers === 'object') {
+          throw new Error(
+            "The object notation for `createSlice.extraReducers` has been removed. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createSlice"
           )
         }
       }
-    }
-    const [
-      extraReducers = {},
-      actionMatchers = [],
-      defaultCaseReducer = undefined,
-    ] =
-      typeof options.extraReducers === 'function'
-        ? executeReducerBuilderCallback(options.extraReducers)
-        : [options.extraReducers]
-
-    const finalCaseReducers = { ...extraReducers, ...sliceCaseReducersByType }
-
-    return createReducer(initialState, (builder) => {
-      for (let key in finalCaseReducers) {
-        builder.addCase(key, finalCaseReducers[key] as CaseReducer<any>)
-      }
-      for (let m of actionMatchers) {
-        builder.addMatcher(m.matcher, m.reducer)
+      const [
+        extraReducers = {},
+        actionMatchers = [],
+        defaultCaseReducer = undefined,
+      ] =
+        typeof options.extraReducers === 'function'
+          ? executeReducerBuilderCallback(options.extraReducers)
+          : [options.extraReducers]
+
+      const finalCaseReducers = {
+        ...extraReducers,
+        ...context.sliceCaseReducersByType,
       }
-      if (defaultCaseReducer) {
-        builder.addDefaultCase(defaultCaseReducer)
+
+      return createReducer(options.initialState, (builder) => {
+        for (let key in finalCaseReducers) {
+          builder.addCase(key, finalCaseReducers[key] as CaseReducer<any>)
+        }
+        for (let sM of context.sliceMatchers) {
+          builder.addMatcher(sM.matcher, sM.reducer)
+        }
+        for (let m of actionMatchers) {
+          builder.addMatcher(m.matcher, m.reducer)
+        }
+        if (defaultCaseReducer) {
+          builder.addDefaultCase(defaultCaseReducer)
+        }
+      })
+    }
+
+    const selectSelf = (state: State) => state
+
+    const injectedSelectorCache = new WeakMap<
+      Slice<State, CaseReducers, Name, ReducerPath, Selectors>,
+      WeakMap<
+        (rootState: any) => State | undefined,
+        Record<string, (rootState: any) => any>
+      >
+    >()
+
+    let _reducer: ReducerWithInitialState<State>
+
+    const slice: Slice<State, CaseReducers, Name, ReducerPath, Selectors> = {
+      name,
+      reducerPath,
+      reducer(state, action) {
+        if (!_reducer) _reducer = buildReducer()
+
+        return _reducer(state, action)
+      },
+      actions: context.actionCreators as any,
+      caseReducers: context.sliceCaseReducersByName as any,
+      getInitialState() {
+        if (!_reducer) _reducer = buildReducer()
+
+        return _reducer.getInitialState()
+      },
+      getSelectors(selectState: (rootState: any) => State = selectSelf) {
+        const selectorCache = emplace(injectedSelectorCache, this, {
+          insert: () => new WeakMap(),
+        })
+
+        return emplace(selectorCache, selectState, {
+          insert: () => {
+            const map: Record<string, Selector<any, any>> = {}
+            for (const [name, selector] of Object.entries(
+              options.selectors ?? {}
+            )) {
+              map[name] = wrapSelector(
+                this,
+                selector,
+                selectState,
+                this !== slice
+              )
+            }
+            return map
+          },
+        }) as any
+      },
+      selectSlice(state) {
+        let sliceState = state[this.reducerPath]
+        if (typeof sliceState === 'undefined') {
+          // check if injectInto has been called
+          if (this !== slice) {
+            sliceState = this.getInitialState()
+          } else if (process.env.NODE_ENV !== 'production') {
+            throw new Error(
+              'selectSlice returned undefined for an uninjected slice reducer'
+            )
+          }
+        }
+        return sliceState
+      },
+      get selectors() {
+        return this.getSelectors(this.selectSlice)
+      },
+      injectInto(injectable, { reducerPath: pathOpt, ...config } = {}) {
+        const reducerPath = pathOpt ?? this.reducerPath
+        injectable.inject({ reducerPath, reducer: this.reducer }, config)
+        return {
+          ...this,
+          reducerPath,
+        } as any
+      },
+    }
+    return slice
+  }
+}
+
+function wrapSelector<State, NewState, S extends Selector<State>>(
+  slice: Slice,
+  selector: S,
+  selectState: Selector<NewState, State>,
+  injected?: boolean
+) {
+  function wrapper(rootState: NewState, ...args: any[]) {
+    let sliceState = selectState.call(slice, rootState)
+    if (typeof sliceState === 'undefined') {
+      if (injected) {
+        sliceState = slice.getInitialState()
+      } else if (process.env.NODE_ENV !== 'production') {
+        throw new Error(
+          'selectState returned undefined for an uninjected slice reducer'
+        )
       }
-    })
+    }
+    return selector(sliceState, ...args)
   }
+  wrapper.unwrapped = selector
+  return wrapper as RemappedSelector<S, NewState>
+}
 
-  let _reducer: ReducerWithInitialState<State>
+/**
+ * A function that accepts an initial state, an object full of reducer
+ * functions, and a "slice name", and automatically generates
+ * action creators and action types that correspond to the
+ * reducers and state.
+ *
+ * @public
+ */
+export const createSlice = buildCreateSlice()
 
-  return {
-    name,
-    reducer(state, action) {
-      if (!_reducer) _reducer = buildReducer()
+interface ReducerHandlingContext<State> {
+  sliceCaseReducersByName: Record<
+    string,
+    | CaseReducer<State, any>
+    | Pick<
+        AsyncThunkSliceReducerDefinition<State, any, any, any>,
+        'fulfilled' | 'rejected' | 'pending' | 'settled'
+      >
+  >
+  sliceCaseReducersByType: Record<string, CaseReducer<State, any>>
+  sliceMatchers: ActionMatcherDescriptionCollection<State>
+  actionCreators: Record<string, Function>
+}
 
-      return _reducer(state, action)
-    },
-    actions: actionCreators as any,
-    caseReducers: sliceCaseReducersByName as any,
-    getInitialState() {
-      if (!_reducer) _reducer = buildReducer()
+interface ReducerHandlingContextMethods<State> {
+  /**
+   * Adds a case reducer to handle a single action type.
+   * @param actionCreator - Either a plain action type string, or an action creator generated by [`createAction`](./createAction) that can be used to determine the action type.
+   * @param reducer - The actual case reducer function.
+   */
+  addCase<ActionCreator extends TypedActionCreator<string>>(
+    actionCreator: ActionCreator,
+    reducer: CaseReducer<State, ReturnType<ActionCreator>>
+  ): ReducerHandlingContextMethods<State>
+  /**
+   * Adds a case reducer to handle a single action type.
+   * @param actionCreator - Either a plain action type string, or an action creator generated by [`createAction`](./createAction) that can be used to determine the action type.
+   * @param reducer - The actual case reducer function.
+   */
+  addCase<Type extends string, A extends Action<Type>>(
+    type: Type,
+    reducer: CaseReducer<State, A>
+  ): ReducerHandlingContextMethods<State>
 
-      return _reducer.getInitialState()
+  /**
+   * Allows you to match incoming actions against your own filter function instead of only the `action.type` property.
+   * @remarks
+   * If multiple matcher reducers match, all of them will be executed in the order
+   * they were defined in - even if a case reducer already matched.
+   * All calls to `builder.addMatcher` must come after any calls to `builder.addCase` and before any calls to `builder.addDefaultCase`.
+   * @param matcher - A matcher function. In TypeScript, this should be a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates)
+   *   function
+   * @param reducer - The actual case reducer function.
+   *
+   */
+  addMatcher<A>(
+    matcher: TypeGuard<A>,
+    reducer: CaseReducer<State, A extends Action ? A : A & Action>
+  ): ReducerHandlingContextMethods<State>
+  /**
+   * Add an action to be exposed under the final `slice.actions` key.
+   * @param name The key to be exposed as.
+   * @param actionCreator The action to expose.
+   * @example
+   * context.exposeAction("addPost", createAction<Post>("addPost"));
+   *
+   * export const { addPost } = slice.actions
+   *
+   * dispatch(addPost(post))
+   */
+  exposeAction(
+    name: string,
+    actionCreator: Function
+  ): ReducerHandlingContextMethods<State>
+  /**
+   * Add a case reducer to be exposed under the final `slice.caseReducers` key.
+   * @param name The key to be exposed as.
+   * @param reducer The reducer to expose.
+   * @example
+   * context.exposeCaseReducer("addPost", (state, action: PayloadAction<Post>) => {
+   *   state.push(action.payload)
+   * })
+   *
+   * slice.caseReducers.addPost([], addPost(post))
+   */
+  exposeCaseReducer(
+    name: string,
+    reducer:
+      | CaseReducer<State, any>
+      | Pick<
+          AsyncThunkSliceReducerDefinition<State, any, any, any>,
+          'fulfilled' | 'rejected' | 'pending' | 'settled'
+        >
+  ): ReducerHandlingContextMethods<State>
+}
+
+interface ReducerDetails {
+  /** The key the reducer was defined under */
+  reducerName: string
+  /** The predefined action type, i.e. `${slice.name}/${reducerName}` */
+  type: string
+  /** Whether create. notation was used when defining reducers */
+  createNotation: boolean
+}
+
+function buildReducerCreators<State>(): ReducerCreators<State> {
+  function asyncThunk(
+    payloadCreator: AsyncThunkPayloadCreator<any, any>,
+    config: AsyncThunkSliceReducerConfig<State, any>
+  ): AsyncThunkSliceReducerDefinition<State, any> {
+    return {
+      _reducerDefinitionType: ReducerType.asyncThunk,
+      payloadCreator,
+      ...config,
+    }
+  }
+  asyncThunk.withTypes = () => asyncThunk
+  return {
+    reducer(caseReducer: CaseReducer<State, any>) {
+      return Object.assign(
+        {
+          // hack so the wrapping function has the same name as the original
+          // we need to create a wrapper so the `reducerDefinitionType` is not assigned to the original
+          [caseReducer.name](...args: Parameters<typeof caseReducer>) {
+            return caseReducer(...args)
+          },
+        }[caseReducer.name],
+        {
+          _reducerDefinitionType: ReducerType.reducer,
+        } as const
+      )
+    },
+    preparedReducer(prepare, reducer) {
+      return {
+        _reducerDefinitionType: ReducerType.reducerWithPrepare,
+        prepare,
+        reducer,
+      }
     },
+    asyncThunk: asyncThunk as any,
+  }
+}
+
+function handleNormalReducerDefinition<State>(
+  { type, reducerName, createNotation }: ReducerDetails,
+  maybeReducerWithPrepare:
+    | CaseReducer<State, { payload: any; type: string }>
+    | CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>,
+  context: ReducerHandlingContextMethods<State>
+) {
+  let caseReducer: CaseReducer<State, any>
+  let prepareCallback: PrepareAction<any> | undefined
+  if ('reducer' in maybeReducerWithPrepare) {
+    if (
+      createNotation &&
+      !isCaseReducerWithPrepareDefinition(maybeReducerWithPrepare)
+    ) {
+      throw new Error(
+        'Please use the `create.preparedReducer` notation for prepared action creators with the `create` notation.'
+      )
+    }
+    caseReducer = maybeReducerWithPrepare.reducer
+    prepareCallback = maybeReducerWithPrepare.prepare
+  } else {
+    caseReducer = maybeReducerWithPrepare
+  }
+  context
+    .addCase(type, caseReducer)
+    .exposeCaseReducer(reducerName, caseReducer)
+    .exposeAction(
+      reducerName,
+      prepareCallback ? createAction(type, prepareCallback) : createAction(type)
+    )
+}
+
+function isAsyncThunkSliceReducerDefinition<State>(
+  reducerDefinition: any
+): reducerDefinition is AsyncThunkSliceReducerDefinition<State, any, any, any> {
+  return reducerDefinition._reducerDefinitionType === ReducerType.asyncThunk
+}
+
+function isCaseReducerWithPrepareDefinition<State>(
+  reducerDefinition: any
+): reducerDefinition is CaseReducerWithPrepareDefinition<State, any> {
+  return (
+    reducerDefinition._reducerDefinitionType === ReducerType.reducerWithPrepare
+  )
+}
+
+function handleThunkCaseReducerDefinition<State>(
+  { type, reducerName }: ReducerDetails,
+  reducerDefinition: AsyncThunkSliceReducerDefinition<State, any, any, any>,
+  context: ReducerHandlingContextMethods<State>,
+  cAT: typeof _createAsyncThunk | undefined
+) {
+  if (!cAT) {
+    throw new Error(
+      'Cannot use `create.asyncThunk` in the built-in `createSlice`. ' +
+        'Use `buildCreateSlice({ creators: { asyncThunk: asyncThunkCreator } })` to create a customised version of `createSlice`.'
+    )
+  }
+  const { payloadCreator, fulfilled, pending, rejected, settled, options } =
+    reducerDefinition
+  const thunk = cAT(type, payloadCreator, options as any)
+  context.exposeAction(reducerName, thunk)
+
+  if (fulfilled) {
+    context.addCase(thunk.fulfilled, fulfilled)
   }
+  if (pending) {
+    context.addCase(thunk.pending, pending)
+  }
+  if (rejected) {
+    context.addCase(thunk.rejected, rejected)
+  }
+  if (settled) {
+    context.addMatcher(thunk.settled, settled)
+  }
+
+  context.exposeCaseReducer(reducerName, {
+    fulfilled: fulfilled || noop,
+    pending: pending || noop,
+    rejected: rejected || noop,
+    settled: settled || noop,
+  })
 }
+
+function noop() {}
diff --git a/packages/toolkit/src/devtoolsExtension.ts b/packages/toolkit/src/devtoolsExtension.ts
--- a/packages/toolkit/src/devtoolsExtension.ts
+++ b/packages/toolkit/src/devtoolsExtension.ts
@@ -90,18 +90,6 @@ export interface DevToolsEnhancerOptions {
    * function which takes `state` object and index as arguments, and should return `state` object back.
    */
   stateSanitizer?: <S>(state: S, index: number) => S
-  /**
-   * *string or array of strings as regex* - actions types to be hidden / shown in the monitors (while passed to the reducers).
-   * If `actionsWhitelist` specified, `actionsBlacklist` is ignored.
-   * @deprecated Use actionsDenylist instead.
-   */
-  actionsBlacklist?: string | string[]
-  /**
-   * *string or array of strings as regex* - actions types to be hidden / shown in the monitors (while passed to the reducers).
-   * If `actionsWhitelist` specified, `actionsBlacklist` is ignored.
-   * @deprecated Use actionsAllowlist instead.
-   */
-  actionsWhitelist?: string | string[]
   /**
    * *string or array of strings as regex* - actions types to be hidden / shown in the monitors (while passed to the reducers).
    * If `actionsAllowlist` specified, `actionsDenylist` is ignored.
@@ -220,7 +208,9 @@ type Compose = typeof compose
 
 interface ComposeWithDevTools {
   (options: DevToolsEnhancerOptions): Compose
-  <StoreExt>(...funcs: StoreEnhancer<StoreExt>[]): StoreEnhancer<StoreExt>
+  <StoreExt extends {}>(
+    ...funcs: StoreEnhancer<StoreExt>[]
+  ): StoreEnhancer<StoreExt>
 }
 
 /**
diff --git a/packages/toolkit/src/dynamicMiddleware/index.ts b/packages/toolkit/src/dynamicMiddleware/index.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/dynamicMiddleware/index.ts
@@ -0,0 +1,95 @@
+import type {
+  Middleware,
+  Dispatch as ReduxDispatch,
+  UnknownAction,
+} from 'redux'
+import { compose } from 'redux'
+import { createAction } from '../createAction'
+import { isAllOf } from '../matchers'
+import { nanoid } from '../nanoid'
+import { emplace, find } from '../utils'
+import type {
+  WithMiddleware,
+  AddMiddleware,
+  MiddlewareEntry,
+  DynamicMiddleware,
+  DynamicMiddlewareInstance,
+} from './types'
+
+const createMiddlewareEntry = <
+  State = any,
+  Dispatch extends ReduxDispatch<UnknownAction> = ReduxDispatch<UnknownAction>
+>(
+  middleware: Middleware<any, State, Dispatch>
+): MiddlewareEntry<State, Dispatch> => ({
+  id: nanoid(),
+  middleware,
+  applied: new Map(),
+})
+
+const matchInstance =
+  (instanceId: string) =>
+  (action: any): action is { meta: { instanceId: string } } =>
+    action?.meta?.instanceId === instanceId
+
+export const createDynamicMiddleware = <
+  State = any,
+  Dispatch extends ReduxDispatch<UnknownAction> = ReduxDispatch<UnknownAction>
+>(): DynamicMiddlewareInstance<State, Dispatch> => {
+  const instanceId = nanoid()
+  const middlewareMap = new Map<string, MiddlewareEntry<State, Dispatch>>()
+
+  const withMiddleware = Object.assign(
+    createAction(
+      'dynamicMiddleware/add',
+      (...middlewares: Middleware<any, State, Dispatch>[]) => ({
+        payload: middlewares,
+        meta: {
+          instanceId,
+        },
+      })
+    ),
+    { withTypes: () => withMiddleware }
+  ) as WithMiddleware<State, Dispatch>
+
+  const addMiddleware = Object.assign(
+    function addMiddleware(...middlewares: Middleware<any, State, Dispatch>[]) {
+      middlewares.forEach((middleware) => {
+        let entry = find(
+          Array.from(middlewareMap.values()),
+          (entry) => entry.middleware === middleware
+        )
+        if (!entry) {
+          entry = createMiddlewareEntry(middleware)
+        }
+        middlewareMap.set(entry.id, entry)
+      })
+    },
+    { withTypes: () => addMiddleware }
+  ) as AddMiddleware<State, Dispatch>
+
+  const getFinalMiddleware: Middleware<{}, State, Dispatch> = (api) => {
+    const appliedMiddleware = Array.from(middlewareMap.values()).map((entry) =>
+      emplace(entry.applied, api, { insert: () => entry.middleware(api) })
+    )
+    return compose(...appliedMiddleware)
+  }
+
+  const isWithMiddleware = isAllOf(withMiddleware, matchInstance(instanceId))
+
+  const middleware: DynamicMiddleware<State, Dispatch> =
+    (api) => (next) => (action) => {
+      if (isWithMiddleware(action)) {
+        addMiddleware(...action.payload)
+        return api.dispatch
+      }
+      return getFinalMiddleware(api)(next)(action)
+    }
+
+  return {
+    middleware,
+    addMiddleware,
+    withMiddleware,
+    instanceId,
+  }
+}
diff --git a/packages/toolkit/src/dynamicMiddleware/react/index.ts b/packages/toolkit/src/dynamicMiddleware/react/index.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/dynamicMiddleware/react/index.ts
@@ -0,0 +1,105 @@
+import type {
+  Action as ReduxAction,
+  UnknownAction,
+  Dispatch as ReduxDispatch,
+  Middleware,
+} from 'redux'
+import type { TSHelpersExtractDispatchExtensions } from '@reduxjs/toolkit'
+import { createDynamicMiddleware as cDM } from '@reduxjs/toolkit'
+import type { ReactReduxContextValue } from 'react-redux'
+import {
+  ReactReduxContext,
+  useDispatch as useDefaultDispatch,
+  createDispatchHook,
+} from 'react-redux'
+import type { Context } from 'react'
+import type {
+  DynamicMiddlewareInstance,
+  GetDispatch,
+  GetState,
+  MiddlewareApiConfig,
+} from '@reduxjs/toolkit'
+
+export type UseDispatchWithMiddlewareHook<
+  Middlewares extends Middleware<any, State, Dispatch>[] = [],
+  State = any,
+  Dispatch extends ReduxDispatch<UnknownAction> = ReduxDispatch<UnknownAction>
+> = () => TSHelpersExtractDispatchExtensions<Middlewares> & Dispatch
+
+export type CreateDispatchWithMiddlewareHook<
+  State = any,
+  Dispatch extends ReduxDispatch<UnknownAction> = ReduxDispatch<UnknownAction>
+> = {
+  <
+    Middlewares extends [
+      Middleware<any, State, Dispatch>,
+      ...Middleware<any, State, Dispatch>[]
+    ]
+  >(
+    ...middlewares: Middlewares
+  ): UseDispatchWithMiddlewareHook<Middlewares, State, Dispatch>
+  withTypes<
+    MiddlewareConfig extends MiddlewareApiConfig
+  >(): CreateDispatchWithMiddlewareHook<
+    GetState<MiddlewareConfig>,
+    GetDispatch<MiddlewareConfig>
+  >
+}
+
+type ActionFromDispatch<Dispatch extends ReduxDispatch<ReduxAction>> =
+  Dispatch extends ReduxDispatch<infer Action> ? Action : never
+
+interface ReactDynamicMiddlewareInstance<
+  State = any,
+  Dispatch extends ReduxDispatch<UnknownAction> = ReduxDispatch<UnknownAction>
+> extends DynamicMiddlewareInstance<State, Dispatch> {
+  createDispatchWithMiddlewareHookFactory: (
+    context?: Context<
+      ReactReduxContextValue<State, ActionFromDispatch<Dispatch>>
+    >
+  ) => CreateDispatchWithMiddlewareHook<State, Dispatch>
+  createDispatchWithMiddlewareHook: CreateDispatchWithMiddlewareHook<
+    State,
+    Dispatch
+  >
+}
+
+export const createDynamicMiddleware = <
+  State = any,
+  Dispatch extends ReduxDispatch<UnknownAction> = ReduxDispatch<UnknownAction>
+>(): ReactDynamicMiddlewareInstance<State, Dispatch> => {
+  const instance = cDM<State, Dispatch>()
+  const createDispatchWithMiddlewareHookFactory = (
+    // @ts-ignore
+    context: Context<
+      ReactReduxContextValue<State, ActionFromDispatch<Dispatch>>
+    > = ReactReduxContext
+  ) => {
+    const useDispatch =
+      // @ts-ignore
+      context === ReactReduxContext
+        ? useDefaultDispatch
+        : createDispatchHook(context)
+    function createDispatchWithMiddlewareHook<
+      Middlewares extends Middleware<any, State, Dispatch>[]
+    >(...middlewares: Middlewares) {
+      instance.addMiddleware(...middlewares)
+      return useDispatch
+    }
+    createDispatchWithMiddlewareHook.withTypes = () =>
+      createDispatchWithMiddlewareHook
+    return createDispatchWithMiddlewareHook as CreateDispatchWithMiddlewareHook<
+      State,
+      Dispatch
+    >
+  }
+
+  const createDispatchWithMiddlewareHook =
+    createDispatchWithMiddlewareHookFactory()
+
+  return {
+    ...instance,
+    createDispatchWithMiddlewareHookFactory,
+    createDispatchWithMiddlewareHook,
+  }
+}
diff --git a/packages/toolkit/src/dynamicMiddleware/types.ts b/packages/toolkit/src/dynamicMiddleware/types.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/dynamicMiddleware/types.ts
@@ -0,0 +1,93 @@
+import type {
+  Middleware,
+  Dispatch as ReduxDispatch,
+  UnknownAction,
+  MiddlewareAPI,
+} from 'redux'
+import type { ExtractDispatchExtensions, FallbackIfUnknown } from '../tsHelpers'
+import type { PayloadAction, BaseActionCreator } from '../createAction'
+
+export type GetMiddlewareApi<MiddlewareApiConfig> = MiddlewareAPI<
+  GetDispatch<MiddlewareApiConfig>,
+  GetState<MiddlewareApiConfig>
+>
+
+export type MiddlewareApiConfig = {
+  state?: unknown
+  dispatch?: ReduxDispatch
+}
+
+// TODO: consolidate with cAT helpers?
+export type GetState<MiddlewareApiConfig> = MiddlewareApiConfig extends {
+  state: infer State
+}
+  ? State
+  : unknown
+
+export type GetDispatch<MiddlewareApiConfig> = MiddlewareApiConfig extends {
+  dispatch: infer Dispatch
+}
+  ? FallbackIfUnknown<Dispatch, ReduxDispatch>
+  : ReduxDispatch
+
+export type AddMiddleware<
+  State = any,
+  Dispatch extends ReduxDispatch<UnknownAction> = ReduxDispatch<UnknownAction>
+> = {
+  (...middlewares: Middleware<any, State, Dispatch>[]): void
+  withTypes<MiddlewareConfig extends MiddlewareApiConfig>(): AddMiddleware<
+    GetState<MiddlewareConfig>,
+    GetDispatch<MiddlewareConfig>
+  >
+}
+
+export interface WithMiddleware<
+  State = any,
+  Dispatch extends ReduxDispatch<UnknownAction> = ReduxDispatch<UnknownAction>
+> extends BaseActionCreator<
+    Middleware<any, State, Dispatch>[],
+    'dynamicMiddleware/add',
+    { instanceId: string }
+  > {
+  <Middlewares extends Middleware<any, State, Dispatch>[]>(
+    ...middlewares: Middlewares
+  ): PayloadAction<Middlewares, 'dynamicMiddleware/add', { instanceId: string }>
+  withTypes<MiddlewareConfig extends MiddlewareApiConfig>(): WithMiddleware<
+    GetState<MiddlewareConfig>,
+    GetDispatch<MiddlewareConfig>
+  >
+}
+
+export interface DynamicDispatch {
+  // return a version of dispatch that knows about middleware
+  <Middlewares extends Middleware<any>[]>(
+    action: PayloadAction<Middlewares, 'dynamicMiddleware/add'>
+  ): ExtractDispatchExtensions<Middlewares> & this
+}
+
+export type MiddlewareEntry<
+  State = unknown,
+  Dispatch extends ReduxDispatch<UnknownAction> = ReduxDispatch<UnknownAction>
+> = {
+  id: string
+  middleware: Middleware<any, State, Dispatch>
+  applied: Map<
+    MiddlewareAPI<Dispatch, State>,
+    ReturnType<Middleware<any, State, Dispatch>>
+  >
+}
+
+export type DynamicMiddleware<
+  State = unknown,
+  Dispatch extends ReduxDispatch<UnknownAction> = ReduxDispatch<UnknownAction>
+> = Middleware<DynamicDispatch, State, Dispatch>
+
+export type DynamicMiddlewareInstance<
+  State = unknown,
+  Dispatch extends ReduxDispatch<UnknownAction> = ReduxDispatch<UnknownAction>
+> = {
+  middleware: DynamicMiddleware<State, Dispatch>
+  addMiddleware: AddMiddleware<State, Dispatch>
+  withMiddleware: WithMiddleware<State, Dispatch>
+  instanceId: string
+}
diff --git a/packages/toolkit/src/entities/create_adapter.ts b/packages/toolkit/src/entities/create_adapter.ts
--- a/packages/toolkit/src/entities/create_adapter.ts
+++ b/packages/toolkit/src/entities/create_adapter.ts
@@ -3,12 +3,27 @@ import type {
   Comparer,
   IdSelector,
   EntityAdapter,
+  EntityId,
 } from './models'
 import { createInitialStateFactory } from './entity_state'
 import { createSelectorsFactory } from './state_selectors'
 import { createSortedStateAdapter } from './sorted_state_adapter'
 import { createUnsortedStateAdapter } from './unsorted_state_adapter'
 
+export interface EntityAdapterOptions<T, Id extends EntityId> {
+  selectId?: IdSelector<T, Id>
+  sortComparer?: false | Comparer<T>
+}
+
+export function createEntityAdapter<T, Id extends EntityId>(options: {
+  selectId: IdSelector<T, Id>
+  sortComparer?: false | Comparer<T>
+}): EntityAdapter<T, Id>
+
+export function createEntityAdapter<T extends { id: EntityId }>(options?: {
+  sortComparer?: false | Comparer<T>
+}): EntityAdapter<T, T['id']>
+
 /**
  *
  * @param options
@@ -17,18 +32,18 @@ import { createUnsortedStateAdapter } from './unsorted_state_adapter'
  */
 export function createEntityAdapter<T>(
   options: {
-    selectId?: IdSelector<T>
+    selectId?: IdSelector<T, EntityId>
     sortComparer?: false | Comparer<T>
   } = {}
-): EntityAdapter<T> {
-  const { selectId, sortComparer }: EntityDefinition<T> = {
+): EntityAdapter<T, EntityId> {
+  const { selectId, sortComparer }: EntityDefinition<T, EntityId> = {
     sortComparer: false,
     selectId: (instance: any) => instance.id,
     ...options,
   }
 
-  const stateFactory = createInitialStateFactory<T>()
-  const selectorsFactory = createSelectorsFactory<T>()
+  const stateFactory = createInitialStateFactory<T, EntityId>()
+  const selectorsFactory = createSelectorsFactory<T, EntityId>()
   const stateAdapter = sortComparer
     ? createSortedStateAdapter(selectId, sortComparer)
     : createUnsortedStateAdapter(selectId)
diff --git a/packages/toolkit/src/entities/entity_state.ts b/packages/toolkit/src/entities/entity_state.ts
--- a/packages/toolkit/src/entities/entity_state.ts
+++ b/packages/toolkit/src/entities/entity_state.ts
@@ -1,17 +1,20 @@
-import type { EntityState } from './models'
+import type { EntityId, EntityState } from './models'
 
-export function getInitialEntityState<V>(): EntityState<V> {
+export function getInitialEntityState<T, Id extends EntityId>(): EntityState<
+  T,
+  Id
+> {
   return {
     ids: [],
-    entities: {},
+    entities: {} as Record<Id, T>,
   }
 }
 
-export function createInitialStateFactory<V>() {
-  function getInitialState(): EntityState<V>
+export function createInitialStateFactory<T, Id extends EntityId>() {
+  function getInitialState(): EntityState<T, Id>
   function getInitialState<S extends object>(
     additionalState: S
-  ): EntityState<V> & S
+  ): EntityState<T, Id> & S
   function getInitialState(additionalState: any = {}): any {
     return Object.assign(getInitialEntityState(), additionalState)
   }
diff --git a/packages/toolkit/src/entities/index.ts b/packages/toolkit/src/entities/index.ts
--- a/packages/toolkit/src/entities/index.ts
+++ b/packages/toolkit/src/entities/index.ts
@@ -1,6 +1,5 @@
 export { createEntityAdapter } from './create_adapter'
 export type {
-  Dictionary,
   EntityState,
   EntityAdapter,
   Update,
diff --git a/packages/toolkit/src/entities/models.ts b/packages/toolkit/src/entities/models.ts
--- a/packages/toolkit/src/entities/models.ts
+++ b/packages/toolkit/src/entities/models.ts
@@ -1,171 +1,182 @@
-import type { PayloadAction } from '../createAction'
-import type { IsAny } from '../tsHelpers'
-
-/**
- * @public
- */
-export type EntityId = number | string
-
-/**
- * @public
- */
-export type Comparer<T> = (a: T, b: T) => number
-
-/**
- * @public
- */
-export type IdSelector<T> = (model: T) => EntityId
-
-/**
- * @public
- */
-export interface DictionaryNum<T> {
-  [id: number]: T | undefined
-}
-
-/**
- * @public
- */
-export interface Dictionary<T> extends DictionaryNum<T> {
-  [id: string]: T | undefined
-}
-
-/**
- * @public
- */
-export type Update<T> = { id: EntityId; changes: Partial<T> }
-
-/**
- * @public
- */
-export interface EntityState<T> {
-  ids: EntityId[]
-  entities: Dictionary<T>
-}
-
-/**
- * @public
- */
-export interface EntityDefinition<T> {
-  selectId: IdSelector<T>
-  sortComparer: false | Comparer<T>
-}
-
-export type PreventAny<S, T> = IsAny<S, EntityState<T>, S>
-
-/**
- * @public
- */
-export interface EntityStateAdapter<T> {
-  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: PreventAny<S, T>,
-    entities: readonly T[] | Record<EntityId, T>
-  ): S
-  addMany<S extends EntityState<T>>(
-    state: PreventAny<S, T>,
-    entities: PayloadAction<readonly T[] | Record<EntityId, T>>
-  ): S
-
-  setOne<S extends EntityState<T>>(state: PreventAny<S, T>, entity: T): S
-  setOne<S extends EntityState<T>>(
-    state: PreventAny<S, T>,
-    action: PayloadAction<T>
-  ): S
-  setMany<S extends EntityState<T>>(
-    state: PreventAny<S, T>,
-    entities: readonly T[] | Record<EntityId, T>
-  ): S
-  setMany<S extends EntityState<T>>(
-    state: PreventAny<S, T>,
-    entities: PayloadAction<readonly T[] | Record<EntityId, T>>
-  ): S
-  setAll<S extends EntityState<T>>(
-    state: PreventAny<S, T>,
-    entities: readonly T[] | Record<EntityId, T>
-  ): S
-  setAll<S extends EntityState<T>>(
-    state: PreventAny<S, T>,
-    entities: PayloadAction<readonly T[] | Record<EntityId, T>>
-  ): 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: PreventAny<S, T>,
-    keys: readonly EntityId[]
-  ): S
-  removeMany<S extends EntityState<T>>(
-    state: PreventAny<S, T>,
-    keys: PayloadAction<readonly EntityId[]>
-  ): S
-
-  removeAll<S extends EntityState<T>>(state: PreventAny<S, T>): S
-
-  updateOne<S extends EntityState<T>>(
-    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: PreventAny<S, T>,
-    updates: ReadonlyArray<Update<T>>
-  ): S
-  updateMany<S extends EntityState<T>>(
-    state: PreventAny<S, T>,
-    updates: PayloadAction<ReadonlyArray<Update<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: PreventAny<S, T>,
-    entities: readonly T[] | Record<EntityId, T>
-  ): S
-  upsertMany<S extends EntityState<T>>(
-    state: PreventAny<S, T>,
-    entities: PayloadAction<readonly T[] | Record<EntityId, T>>
-  ): S
-}
-
-/**
- * @public
- */
-export interface EntitySelectors<T, V> {
-  selectIds: (state: V) => EntityId[]
-  selectEntities: (state: V) => Dictionary<T>
-  selectAll: (state: V) => T[]
-  selectTotal: (state: V) => number
-  selectById: (state: V, id: EntityId) => T | undefined
-}
-
-/**
- * @public
- */
-export interface EntityAdapter<T> extends EntityStateAdapter<T> {
-  selectId: IdSelector<T>
-  sortComparer: false | Comparer<T>
-  getInitialState(): EntityState<T>
-  getInitialState<S extends object>(state: S): EntityState<T> & S
-  getSelectors(): EntitySelectors<T, EntityState<T>>
-  getSelectors<V>(
-    selectState: (state: V) => EntityState<T>
-  ): EntitySelectors<T, V>
-}
+import type { UncheckedIndexedAccess } from '../uncheckedindexed'
+import type { Draft } from 'immer'
+import type { PayloadAction } from '../createAction'
+import type { GetSelectorsOptions } from './state_selectors'
+import type { CastAny, Id as Compute } from '../tsHelpers'
+
+/**
+ * @public
+ */
+export type EntityId = number | string
+
+/**
+ * @public
+ */
+export type Comparer<T> = (a: T, b: T) => number
+
+/**
+ * @public
+ */
+export type IdSelector<T, Id extends EntityId> = (model: T) => Id
+
+/**
+ * @public
+ */
+export type Update<T, Id extends EntityId> = { id: Id; changes: Partial<T> }
+
+/**
+ * @public
+ */
+export interface EntityState<T, Id extends EntityId> {
+  ids: Id[]
+  entities: Record<Id, T>
+}
+
+/**
+ * @public
+ */
+export interface EntityDefinition<T, Id extends EntityId> {
+  selectId: IdSelector<T, Id>
+  sortComparer: false | Comparer<T>
+}
+
+export type PreventAny<S, T, Id extends EntityId> = CastAny<
+  S,
+  EntityState<T, Id>
+  >
+
+export type DraftableEntityState<T, Id extends EntityId> = EntityState<T, Id> | Draft<EntityState<T, Id>>
+
+/**
+ * @public
+ */
+export interface EntityStateAdapter<T, Id extends EntityId> {
+  addOne<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    entity: T
+  ): S
+  addOne<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    action: PayloadAction<T>
+  ): S
+
+  addMany<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    entities: readonly T[] | Record<Id, T>
+  ): S
+  addMany<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    entities: PayloadAction<readonly T[] | Record<Id, T>>
+  ): S
+
+  setOne<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    entity: T
+  ): S
+  setOne<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    action: PayloadAction<T>
+  ): S
+  setMany<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    entities: readonly T[] | Record<Id, T>
+  ): S
+  setMany<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    entities: PayloadAction<readonly T[] | Record<Id, T>>
+  ): S
+  setAll<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    entities: readonly T[] | Record<Id, T>
+  ): S
+  setAll<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    entities: PayloadAction<readonly T[] | Record<Id, T>>
+  ): S
+
+  removeOne<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    key: Id
+  ): S
+  removeOne<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    key: PayloadAction<Id>
+  ): S
+
+  removeMany<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    keys: readonly Id[]
+  ): S
+  removeMany<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    keys: PayloadAction<readonly Id[]>
+  ): S
+
+  removeAll<S extends DraftableEntityState<T, Id>>(state: PreventAny<S, T, Id>): S
+
+  updateOne<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    update: Update<T, Id>
+  ): S
+  updateOne<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    update: PayloadAction<Update<T, Id>>
+  ): S
+
+  updateMany<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    updates: ReadonlyArray<Update<T, Id>>
+  ): S
+  updateMany<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    updates: PayloadAction<ReadonlyArray<Update<T, Id>>>
+  ): S
+
+  upsertOne<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    entity: T
+  ): S
+  upsertOne<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    entity: PayloadAction<T>
+  ): S
+
+  upsertMany<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    entities: readonly T[] | Record<Id, T>
+  ): S
+  upsertMany<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>,
+    entities: PayloadAction<readonly T[] | Record<Id, T>>
+  ): S
+}
+
+/**
+ * @public
+ */
+export interface EntitySelectors<T, V, Id extends EntityId> {
+  selectIds: (state: V) => Id[]
+  selectEntities: (state: V) => Record<Id, T>
+  selectAll: (state: V) => T[]
+  selectTotal: (state: V) => number
+  selectById: (state: V, id: Id) => Compute<UncheckedIndexedAccess<T>>
+}
+
+/**
+ * @public
+ */
+export interface EntityAdapter<T, Id extends EntityId>
+  extends EntityStateAdapter<T, Id> {
+  selectId: IdSelector<T, Id>
+  sortComparer: false | Comparer<T>
+  getInitialState(): EntityState<T, Id>
+  getInitialState<S extends object>(state: S): EntityState<T, Id> & S
+  getSelectors(
+    selectState?: undefined,
+    options?: GetSelectorsOptions
+  ): EntitySelectors<T, EntityState<T, Id>, Id>
+  getSelectors<V>(
+    selectState: (state: V) => EntityState<T, Id>,
+    options?: GetSelectorsOptions
+  ): EntitySelectors<T, V, Id>
+}
diff --git a/packages/toolkit/src/entities/sorted_state_adapter.ts b/packages/toolkit/src/entities/sorted_state_adapter.ts
--- a/packages/toolkit/src/entities/sorted_state_adapter.ts
+++ b/packages/toolkit/src/entities/sorted_state_adapter.ts
@@ -1,168 +1,168 @@
-import type {
-  EntityState,
-  IdSelector,
-  Comparer,
-  EntityStateAdapter,
-  Update,
-  EntityId,
-} from './models'
-import { createStateOperator } from './state_adapter'
-import { createUnsortedStateAdapter } from './unsorted_state_adapter'
-import {
-  selectIdValue,
-  ensureEntitiesArray,
-  splitAddedUpdatedEntities,
-} from './utils'
-
-export function createSortedStateAdapter<T>(
-  selectId: IdSelector<T>,
-  sort: Comparer<T>
-): EntityStateAdapter<T> {
-  type R = EntityState<T>
-
-  const { removeOne, removeMany, removeAll } =
-    createUnsortedStateAdapter(selectId)
-
-  function addOneMutably(entity: T, state: R): void {
-    return addManyMutably([entity], state)
-  }
-
-  function addManyMutably(
-    newEntities: readonly T[] | Record<EntityId, T>,
-    state: R
-  ): void {
-    newEntities = ensureEntitiesArray(newEntities)
-
-    const models = newEntities.filter(
-      (model) => !(selectIdValue(model, selectId) in state.entities)
-    )
-
-    if (models.length !== 0) {
-      merge(models, state)
-    }
-  }
-
-  function setOneMutably(entity: T, state: R): void {
-    return setManyMutably([entity], state)
-  }
-
-  function setManyMutably(
-    newEntities: readonly T[] | Record<EntityId, T>,
-    state: R
-  ): void {
-    newEntities = ensureEntitiesArray(newEntities)
-    if (newEntities.length !== 0) {
-      merge(newEntities, state)
-    }
-  }
-
-  function setAllMutably(
-    newEntities: readonly T[] | Record<EntityId, T>,
-    state: R
-  ): void {
-    newEntities = ensureEntitiesArray(newEntities)
-    state.entities = {}
-    state.ids = []
-
-    addManyMutably(newEntities, state)
-  }
-
-  function updateOneMutably(update: Update<T>, state: R): void {
-    return updateManyMutably([update], state)
-  }
-
-  function updateManyMutably(
-    updates: ReadonlyArray<Update<T>>,
-    state: R
-  ): void {
-    let appliedUpdates = false
-
-    for (let update of updates) {
-      const entity = state.entities[update.id]
-      if (!entity) {
-        continue
-      }
-
-      appliedUpdates = true
-
-      Object.assign(entity, update.changes)
-      const newId = selectId(entity)
-      if (update.id !== newId) {
-        delete state.entities[update.id]
-        state.entities[newId] = entity
-      }
-    }
-
-    if (appliedUpdates) {
-      resortEntities(state)
-    }
-  }
-
-  function upsertOneMutably(entity: T, state: R): void {
-    return upsertManyMutably([entity], state)
-  }
-
-  function upsertManyMutably(
-    newEntities: readonly T[] | Record<EntityId, T>,
-    state: R
-  ): void {
-    const [added, updated] = splitAddedUpdatedEntities<T>(
-      newEntities,
-      selectId,
-      state
-    )
-
-    updateManyMutably(updated, state)
-    addManyMutably(added, state)
-  }
-
-  function areArraysEqual(a: readonly unknown[], b: readonly unknown[]) {
-    if (a.length !== b.length) {
-      return false
-    }
-
-    for (let i = 0; i < a.length && i < b.length; i++) {
-      if (a[i] === b[i]) {
-        continue
-      }
-      return false
-    }
-    return true
-  }
-
-  function merge(models: readonly T[], state: R): void {
-    // Insert/overwrite all new/updated
-    models.forEach((model) => {
-      state.entities[selectId(model)] = model
-    })
-
-    resortEntities(state)
-  }
-
-  function resortEntities(state: R) {
-    const allEntities = Object.values(state.entities) as T[]
-    allEntities.sort(sort)
-
-    const newSortedIds = allEntities.map(selectId)
-    const { ids } = state
-
-    if (!areArraysEqual(ids, newSortedIds)) {
-      state.ids = newSortedIds
-    }
-  }
-
-  return {
-    removeOne,
-    removeMany,
-    removeAll,
-    addOne: createStateOperator(addOneMutably),
-    updateOne: createStateOperator(updateOneMutably),
-    upsertOne: createStateOperator(upsertOneMutably),
-    setOne: createStateOperator(setOneMutably),
-    setMany: createStateOperator(setManyMutably),
-    setAll: createStateOperator(setAllMutably),
-    addMany: createStateOperator(addManyMutably),
-    updateMany: createStateOperator(updateManyMutably),
-    upsertMany: createStateOperator(upsertManyMutably),
-  }
-}
+import type {
+  IdSelector,
+  Comparer,
+  EntityStateAdapter,
+  Update,
+  EntityId,
+  DraftableEntityState
+} from './models'
+import { createStateOperator } from './state_adapter'
+import { createUnsortedStateAdapter } from './unsorted_state_adapter'
+import {
+  selectIdValue,
+  ensureEntitiesArray,
+  splitAddedUpdatedEntities,
+} from './utils'
+
+export function createSortedStateAdapter<T, Id extends EntityId>(
+  selectId: IdSelector<T, Id>,
+  sort: Comparer<T>
+): EntityStateAdapter<T, Id> {
+  type R = DraftableEntityState<T, Id>
+
+  const { removeOne, removeMany, removeAll } =
+    createUnsortedStateAdapter(selectId)
+
+  function addOneMutably(entity: T, state: R): void {
+    return addManyMutably([entity], state)
+  }
+
+  function addManyMutably(
+    newEntities: readonly T[] | Record<Id, T>,
+    state: R
+  ): void {
+    newEntities = ensureEntitiesArray(newEntities)
+
+    const models = newEntities.filter(
+      (model) => !(selectIdValue(model, selectId) in state.entities)
+    )
+
+    if (models.length !== 0) {
+      merge(models, state)
+    }
+  }
+
+  function setOneMutably(entity: T, state: R): void {
+    return setManyMutably([entity], state)
+  }
+
+  function setManyMutably(
+    newEntities: readonly T[] | Record<Id, T>,
+    state: R
+  ): void {
+    newEntities = ensureEntitiesArray(newEntities)
+    if (newEntities.length !== 0) {
+      merge(newEntities, state)
+    }
+  }
+
+  function setAllMutably(
+    newEntities: readonly T[] | Record<Id, T>,
+    state: R
+  ): void {
+    newEntities = ensureEntitiesArray(newEntities)
+    state.entities = {} as Record<Id, T>
+    state.ids = []
+
+    addManyMutably(newEntities, state)
+  }
+
+  function updateOneMutably(update: Update<T, Id>, state: R): void {
+    return updateManyMutably([update], state)
+  }
+
+  function updateManyMutably(
+    updates: ReadonlyArray<Update<T, Id>>,
+    state: R
+  ): void {
+    let appliedUpdates = false
+
+    for (let update of updates) {
+      const entity: T | undefined = (state.entities as Record<Id, T>)[update.id]
+      if (!entity) {
+        continue
+      }
+
+      appliedUpdates = true
+
+      Object.assign(entity, update.changes)
+      const newId = selectId(entity)
+      if (update.id !== newId) {
+        delete (state.entities as Record<Id, T>)[update.id];
+        (state.entities as Record<Id, T>)[newId] = entity
+      }
+    }
+
+    if (appliedUpdates) {
+      resortEntities(state)
+    }
+  }
+
+  function upsertOneMutably(entity: T, state: R): void {
+    return upsertManyMutably([entity], state)
+  }
+
+  function upsertManyMutably(
+    newEntities: readonly T[] | Record<Id, T>,
+    state: R
+  ): void {
+    const [added, updated] = splitAddedUpdatedEntities<T, Id>(
+      newEntities,
+      selectId,
+      state
+    )
+
+    updateManyMutably(updated, state)
+    addManyMutably(added, state)
+  }
+
+  function areArraysEqual(a: readonly unknown[], b: readonly unknown[]) {
+    if (a.length !== b.length) {
+      return false
+    }
+
+    for (let i = 0; i < a.length && i < b.length; i++) {
+      if (a[i] === b[i]) {
+        continue
+      }
+      return false
+    }
+    return true
+  }
+
+  function merge(models: readonly T[], state: R): void {
+    // Insert/overwrite all new/updated
+    models.forEach((model) => {
+      (state.entities as Record<Id, T>)[selectId(model)] = model
+    })
+
+    resortEntities(state)
+  }
+
+  function resortEntities(state: R) {
+    const allEntities = Object.values(state.entities) as T[]
+    allEntities.sort(sort)
+
+    const newSortedIds = allEntities.map(selectId)
+    const { ids } = state
+
+    if (!areArraysEqual(ids, newSortedIds)) {
+      state.ids = newSortedIds
+    }
+  }
+
+  return {
+    removeOne,
+    removeMany,
+    removeAll,
+    addOne: createStateOperator(addOneMutably),
+    updateOne: createStateOperator(updateOneMutably),
+    upsertOne: createStateOperator(upsertOneMutably),
+    setOne: createStateOperator(setOneMutably),
+    setMany: createStateOperator(setManyMutably),
+    setAll: createStateOperator(setAllMutably),
+    addMany: createStateOperator(addManyMutably),
+    updateMany: createStateOperator(updateManyMutably),
+    upsertMany: createStateOperator(upsertManyMutably),
+  }
+}
diff --git a/packages/toolkit/src/entities/state_adapter.ts b/packages/toolkit/src/entities/state_adapter.ts
--- a/packages/toolkit/src/entities/state_adapter.ts
+++ b/packages/toolkit/src/entities/state_adapter.ts
@@ -1,27 +1,29 @@
-import createNextState, { isDraft } from 'immer'
-import type { EntityState, PreventAny } from './models'
+import { produce as createNextState, isDraft } from 'immer'
+import type {  Draft } from 'immer'
+import type { EntityId, DraftableEntityState, PreventAny } from './models'
 import type { PayloadAction } from '../createAction'
 import { isFSA } from '../createAction'
-import { IsAny } from '../tsHelpers'
 
-export function createSingleArgumentStateOperator<V>(
-  mutator: (state: EntityState<V>) => void
+export const isDraftTyped = isDraft as <T>(value: T | Draft<T>) => value is Draft<T>
+
+export function createSingleArgumentStateOperator<T, Id extends EntityId>(
+  mutator: (state: DraftableEntityState<T, Id>) => void
 ) {
-  const operator = createStateOperator((_: undefined, state: EntityState<V>) =>
-    mutator(state)
+  const operator = createStateOperator(
+    (_: undefined, state: DraftableEntityState<T, Id>) => mutator(state)
   )
 
-  return function operation<S extends EntityState<V>>(
-    state: PreventAny<S, V>
+  return function operation<S extends DraftableEntityState<T, Id>>(
+    state: PreventAny<S, T, Id>
   ): S {
     return operator(state as S, undefined)
   }
 }
 
-export function createStateOperator<V, R>(
-  mutator: (arg: R, state: EntityState<V>) => void
+export function createStateOperator<T, Id extends EntityId, R>(
+  mutator: (arg: R, state: DraftableEntityState<T, Id>) => void
 ) {
-  return function operation<S extends EntityState<V>>(
+  return function operation<S extends DraftableEntityState<T, Id>>(
     state: S,
     arg: R | PayloadAction<R>
   ): S {
@@ -31,7 +33,7 @@ export function createStateOperator<V, R>(
       return isFSA(arg)
     }
 
-    const runMutator = (draft: EntityState<V>) => {
+    const runMutator = (draft: DraftableEntityState<T, Id>) => {
       if (isPayloadActionArgument(arg)) {
         mutator(arg.payload, draft)
       } else {
@@ -39,7 +41,7 @@ export function createStateOperator<V, R>(
       }
     }
 
-    if (isDraft(state)) {
+    if (isDraftTyped<DraftableEntityState<T, Id>>(state)) {
       // we must already be inside a `createNextState` call, likely because
       // this is being wrapped in `createReducer` or `createSlice`.
       // It's safe to just pass the draft to the mutator.
@@ -47,11 +49,8 @@ export function createStateOperator<V, R>(
 
       // since it's a draft, we'll just return it
       return state
-    } 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, runMutator)
     }
+    
+    return createNextState(state, runMutator)
   }
 }
diff --git a/packages/toolkit/src/entities/state_selectors.ts b/packages/toolkit/src/entities/state_selectors.ts
--- a/packages/toolkit/src/entities/state_selectors.ts
+++ b/packages/toolkit/src/entities/state_selectors.ts
@@ -1,35 +1,47 @@
-import type { Selector } from 'reselect'
+import type { CreateSelectorFunction, Selector, createSelector } from 'reselect'
 import { createDraftSafeSelector } from '../createDraftSafeSelector'
-import type {
-  EntityState,
-  EntitySelectors,
-  Dictionary,
-  EntityId,
-} from './models'
+import type { EntityState, EntitySelectors, EntityId } from './models'
 
-export function createSelectorsFactory<T>() {
-  function getSelectors(): EntitySelectors<T, EntityState<T>>
+type AnyFunction = (...args: any) => any
+type AnyCreateSelectorFunction = CreateSelectorFunction<
+  <F extends AnyFunction>(f: F) => F,
+  <F extends AnyFunction>(f: F) => F
+>
+
+export interface GetSelectorsOptions {
+  createSelector?: AnyCreateSelectorFunction
+}
+
+export function createSelectorsFactory<T, Id extends EntityId>() {
+  function getSelectors(
+    selectState?: undefined,
+    options?: GetSelectorsOptions
+  ): EntitySelectors<T, EntityState<T, Id>, Id>
   function getSelectors<V>(
-    selectState: (state: V) => EntityState<T>
-  ): EntitySelectors<T, V>
+    selectState: (state: V) => EntityState<T, Id>,
+    options?: GetSelectorsOptions
+  ): EntitySelectors<T, V, Id>
   function getSelectors<V>(
-    selectState?: (state: V) => EntityState<T>
-  ): EntitySelectors<T, any> {
-    const selectIds = (state: EntityState<T>) => state.ids
+    selectState?: (state: V) => EntityState<T, Id>,
+    options: GetSelectorsOptions = {}
+  ): EntitySelectors<T, any, Id> {
+    const { createSelector = createDraftSafeSelector as AnyCreateSelectorFunction } = options
+
+    const selectIds = (state: EntityState<T, Id>) => state.ids
 
-    const selectEntities = (state: EntityState<T>) => state.entities
+    const selectEntities = (state: EntityState<T, Id>) => state.entities
 
-    const selectAll = createDraftSafeSelector(
+    const selectAll = createSelector(
       selectIds,
       selectEntities,
       (ids, entities): T[] => ids.map((id) => entities[id]!)
     )
 
-    const selectId = (_: unknown, id: EntityId) => id
+    const selectId = (_: unknown, id: Id) => id
 
-    const selectById = (entities: Dictionary<T>, id: EntityId) => entities[id]
+    const selectById = (entities: Record<Id, T>, id: Id) => entities[id]
 
-    const selectTotal = createDraftSafeSelector(selectIds, (ids) => ids.length)
+    const selectTotal = createSelector(selectIds, (ids) => ids.length)
 
     if (!selectState) {
       return {
@@ -37,25 +49,21 @@ export function createSelectorsFactory<T>() {
         selectEntities,
         selectAll,
         selectTotal,
-        selectById: createDraftSafeSelector(
-          selectEntities,
-          selectId,
-          selectById
-        ),
+        selectById: createSelector(selectEntities, selectId, selectById),
       }
     }
 
-    const selectGlobalizedEntities = createDraftSafeSelector(
-      selectState as Selector<V, EntityState<T>>,
+    const selectGlobalizedEntities = createSelector(
+      selectState as Selector<V, EntityState<T, Id>>,
       selectEntities
     )
 
     return {
-      selectIds: createDraftSafeSelector(selectState, selectIds),
+      selectIds: createSelector(selectState, selectIds),
       selectEntities: selectGlobalizedEntities,
-      selectAll: createDraftSafeSelector(selectState, selectAll),
-      selectTotal: createDraftSafeSelector(selectState, selectTotal),
-      selectById: createDraftSafeSelector(
+      selectAll: createSelector(selectState, selectAll),
+      selectTotal: createSelector(selectState, selectTotal),
+      selectById: createSelector(
         selectGlobalizedEntities,
         selectId,
         selectById
diff --git a/packages/toolkit/src/entities/unsorted_state_adapter.ts b/packages/toolkit/src/entities/unsorted_state_adapter.ts
--- a/packages/toolkit/src/entities/unsorted_state_adapter.ts
+++ b/packages/toolkit/src/entities/unsorted_state_adapter.ts
@@ -1,9 +1,10 @@
+import type { Draft } from 'immer'
 import type {
-  EntityState,
   EntityStateAdapter,
   IdSelector,
   Update,
   EntityId,
+  DraftableEntityState
 } from './models'
 import {
   createStateOperator,
@@ -15,10 +16,10 @@ import {
   splitAddedUpdatedEntities,
 } from './utils'
 
-export function createUnsortedStateAdapter<T>(
-  selectId: IdSelector<T>
-): EntityStateAdapter<T> {
-  type R = EntityState<T>
+export function createUnsortedStateAdapter<T, Id extends EntityId>(
+  selectId: IdSelector<T, Id>
+): EntityStateAdapter<T, Id> {
+  type R = DraftableEntityState<T, Id>
 
   function addOneMutably(entity: T, state: R): void {
     const key = selectIdValue(entity, selectId)
@@ -27,12 +28,12 @@ export function createUnsortedStateAdapter<T>(
       return
     }
 
-    state.ids.push(key)
-    state.entities[key] = entity
+    state.ids.push(key as Id & Draft<Id>);
+    (state.entities as Record<Id, T>)[key] = entity
   }
 
   function addManyMutably(
-    newEntities: readonly T[] | Record<EntityId, T>,
+    newEntities: readonly T[] | Record<Id, T>,
     state: R
   ): void {
     newEntities = ensureEntitiesArray(newEntities)
@@ -45,13 +46,13 @@ export function createUnsortedStateAdapter<T>(
   function setOneMutably(entity: T, state: R): void {
     const key = selectIdValue(entity, selectId)
     if (!(key in state.entities)) {
-      state.ids.push(key)
+      state.ids.push(key as Id & Draft<Id>);
     }
-    state.entities[key] = entity
+    (state.entities as Record<Id, T>)[key] = entity
   }
 
   function setManyMutably(
-    newEntities: readonly T[] | Record<EntityId, T>,
+    newEntities: readonly T[] | Record<Id, T>,
     state: R
   ): void {
     newEntities = ensureEntitiesArray(newEntities)
@@ -61,33 +62,33 @@ export function createUnsortedStateAdapter<T>(
   }
 
   function setAllMutably(
-    newEntities: readonly T[] | Record<EntityId, T>,
+    newEntities: readonly T[] | Record<Id, T>,
     state: R
   ): void {
     newEntities = ensureEntitiesArray(newEntities)
 
     state.ids = []
-    state.entities = {}
+    state.entities = {} as Record<Id, T>
 
     addManyMutably(newEntities, state)
   }
 
-  function removeOneMutably(key: EntityId, state: R): void {
+  function removeOneMutably(key: Id, state: R): void {
     return removeManyMutably([key], state)
   }
 
-  function removeManyMutably(keys: readonly EntityId[], state: R): void {
+  function removeManyMutably(keys: readonly Id[], state: R): void {
     let didMutate = false
 
     keys.forEach((key) => {
       if (key in state.entities) {
-        delete state.entities[key]
+        delete (state.entities as Record<Id, T>)[key]
         didMutate = true
       }
     })
 
     if (didMutate) {
-      state.ids = state.ids.filter((id) => id in state.entities)
+      state.ids = (state.ids as Id[]).filter((id) => id in state.entities) as Id[] | Draft<Id[]>
     }
   }
 
@@ -99,36 +100,39 @@ export function createUnsortedStateAdapter<T>(
   }
 
   function takeNewKey(
-    keys: { [id: string]: EntityId },
-    update: Update<T>,
+    keys: { [id: string]: Id },
+    update: Update<T, Id>,
     state: R
   ): boolean {
-    const original = state.entities[update.id]
+    const original: T | undefined = (state.entities as Record<Id, T>)[update.id]
+    if (original === undefined) {
+      return false
+    }
     const updated: T = Object.assign({}, original, update.changes)
     const newKey = selectIdValue(updated, selectId)
     const hasNewKey = newKey !== update.id
 
     if (hasNewKey) {
       keys[update.id] = newKey
-      delete state.entities[update.id]
+      delete (state.entities as Record<Id, T>)[update.id]
     }
 
-    state.entities[newKey] = updated
+    (state.entities as Record<Id, T>)[newKey] = updated
 
     return hasNewKey
   }
 
-  function updateOneMutably(update: Update<T>, state: R): void {
+  function updateOneMutably(update: Update<T, Id>, state: R): void {
     return updateManyMutably([update], state)
   }
 
   function updateManyMutably(
-    updates: ReadonlyArray<Update<T>>,
+    updates: ReadonlyArray<Update<T, Id>>,
     state: R
   ): void {
-    const newKeys: { [id: string]: EntityId } = {}
+    const newKeys: { [id: string]: Id } = {}
 
-    const updatesPerEntity: { [id: string]: Update<T> } = {}
+    const updatesPerEntity: { [id: string]: Update<T, Id> } = {}
 
     updates.forEach((update) => {
       // Only apply updates to entities that currently exist
@@ -158,7 +162,9 @@ export function createUnsortedStateAdapter<T>(
         0
 
       if (didMutateIds) {
-        state.ids = Object.keys(state.entities)
+        state.ids = Object.values(state.entities).map((e) =>
+          selectIdValue(e as T, selectId)
+        )
       }
     }
   }
@@ -168,10 +174,10 @@ export function createUnsortedStateAdapter<T>(
   }
 
   function upsertManyMutably(
-    newEntities: readonly T[] | Record<EntityId, T>,
+    newEntities: readonly T[] | Record<Id, T>,
     state: R
   ): void {
-    const [added, updated] = splitAddedUpdatedEntities<T>(
+    const [added, updated] = splitAddedUpdatedEntities<T, Id>(
       newEntities,
       selectId,
       state
diff --git a/packages/toolkit/src/entities/utils.ts b/packages/toolkit/src/entities/utils.ts
--- a/packages/toolkit/src/entities/utils.ts
+++ b/packages/toolkit/src/entities/utils.ts
@@ -1,6 +1,9 @@
-import type { EntityState, IdSelector, Update, EntityId } from './models'
+import type { IdSelector, Update, EntityId, DraftableEntityState } from './models'
 
-export function selectIdValue<T>(entity: T, selectId: IdSelector<T>) {
+export function selectIdValue<T, Id extends EntityId>(
+  entity: T,
+  selectId: IdSelector<T, Id>
+) {
   const key = selectId(entity)
 
   if (process.env.NODE_ENV !== 'production' && key === undefined) {
@@ -17,8 +20,8 @@ export function selectIdValue<T>(entity: T, selectId: IdSelector<T>) {
   return key
 }
 
-export function ensureEntitiesArray<T>(
-  entities: readonly T[] | Record<EntityId, T>
+export function ensureEntitiesArray<T, Id extends EntityId>(
+  entities: readonly T[] | Record<Id, T>
 ): readonly T[] {
   if (!Array.isArray(entities)) {
     entities = Object.values(entities)
@@ -27,15 +30,15 @@ export function ensureEntitiesArray<T>(
   return entities
 }
 
-export function splitAddedUpdatedEntities<T>(
-  newEntities: readonly T[] | Record<EntityId, T>,
-  selectId: IdSelector<T>,
-  state: EntityState<T>
-): [T[], Update<T>[]] {
+export function splitAddedUpdatedEntities<T, Id extends EntityId>(
+  newEntities: readonly T[] | Record<Id, T>,
+  selectId: IdSelector<T, Id>,
+  state: DraftableEntityState<T, Id>
+): [T[], Update<T, Id>[]] {
   newEntities = ensureEntitiesArray(newEntities)
 
   const added: T[] = []
-  const updated: Update<T>[] = []
+  const updated: Update<T, Id>[] = []
 
   for (const entity of newEntities) {
     const id = selectIdValue(entity, selectId)
diff --git a/packages/toolkit/src/formatProdErrorMessage.ts b/packages/toolkit/src/formatProdErrorMessage.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/formatProdErrorMessage.ts
@@ -0,0 +1,13 @@
+/**
+ * Adapted from React: https://github.com/facebook/react/blob/master/packages/shared/formatProdErrorMessage.js
+ *
+ * Do not require this module directly! Use normal throw error calls. These messages will be replaced with error codes
+ * during build.
+ * @param {number} code
+ */
+export function formatProdErrorMessage(code: number) {
+  return (
+    `Minified Redux Toolkit error #${code}; visit https://redux-toolkit.js.org/Errors?code=${code} for the full message or ` +
+    'use the non-minified dev environment for full errors. '
+  )
+}
diff --git a/packages/toolkit/src/getDefaultEnhancers.ts b/packages/toolkit/src/getDefaultEnhancers.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/getDefaultEnhancers.ts
@@ -0,0 +1,29 @@
+import type { StoreEnhancer } from 'redux'
+import type { AutoBatchOptions } from './autoBatchEnhancer'
+import { autoBatchEnhancer } from './autoBatchEnhancer'
+import { Tuple } from './utils'
+import type { Middlewares } from './configureStore'
+import type { ExtractDispatchExtensions } from './tsHelpers'
+
+type GetDefaultEnhancersOptions = {
+  autoBatch?: boolean | AutoBatchOptions
+}
+
+export type GetDefaultEnhancers<M extends Middlewares<any>> = (
+  options?: GetDefaultEnhancersOptions
+) => Tuple<[StoreEnhancer<{ dispatch: ExtractDispatchExtensions<M> }>]>
+
+export const buildGetDefaultEnhancers = <M extends Middlewares<any>>(
+  middlewareEnhancer: StoreEnhancer<{ dispatch: ExtractDispatchExtensions<M> }>
+): GetDefaultEnhancers<M> =>
+  function getDefaultEnhancers(options) {
+    const { autoBatch = true } = options ?? {}
+
+    let enhancerArray = new Tuple<StoreEnhancer[]>(middlewareEnhancer)
+    if (autoBatch) {
+      enhancerArray.push(
+        autoBatchEnhancer(typeof autoBatch === 'object' ? autoBatch : undefined)
+      )
+    }
+    return enhancerArray as any
+  }
diff --git a/packages/toolkit/src/getDefaultMiddleware.ts b/packages/toolkit/src/getDefaultMiddleware.ts
--- a/packages/toolkit/src/getDefaultMiddleware.ts
+++ b/packages/toolkit/src/getDefaultMiddleware.ts
@@ -1,6 +1,6 @@
-import type { Middleware, AnyAction } from 'redux'
+import type { Middleware, UnknownAction } from 'redux'
 import type { ThunkMiddleware } from 'redux-thunk'
-import thunkMiddleware from 'redux-thunk'
+import { thunk as thunkMiddleware, withExtraArgument } from 'redux-thunk'
 import type { ActionCreatorInvariantMiddlewareOptions } from './actionCreatorInvariantMiddleware'
 import { createActionCreatorInvariantMiddleware } from './actionCreatorInvariantMiddleware'
 import type { ImmutableStateInvariantMiddlewareOptions } from './immutableStateInvariantMiddleware'
@@ -11,7 +11,7 @@ import { createImmutableStateInvariantMiddleware } from './immutableStateInvaria
 import type { SerializableStateInvariantMiddlewareOptions } from './serializableStateInvariantMiddleware'
 import { createSerializableStateInvariantMiddleware } from './serializableStateInvariantMiddleware'
 import type { ExcludeFromTuple } from './tsHelpers'
-import { MiddlewareArray } from './utils'
+import { Tuple } from './utils'
 
 function isBoolean(x: any): x is boolean {
   return typeof x === 'boolean'
@@ -36,11 +36,11 @@ export type ThunkMiddlewareFor<
 }
   ? never
   : O extends { thunk: { extraArgument: infer E } }
-  ? ThunkMiddleware<S, AnyAction, E>
-  : ThunkMiddleware<S, AnyAction>
+  ? ThunkMiddleware<S, UnknownAction, E>
+  : ThunkMiddleware<S, UnknownAction>
 
-export type CurriedGetDefaultMiddleware<S = any> = <
-  O extends Partial<GetDefaultMiddlewareOptions> = {
+export type GetDefaultMiddleware<S = any> = <
+  O extends GetDefaultMiddlewareOptions = {
     thunk: true
     immutableCheck: true
     serializableCheck: true
@@ -48,96 +48,66 @@ export type CurriedGetDefaultMiddleware<S = any> = <
   }
 >(
   options?: O
-) => MiddlewareArray<ExcludeFromTuple<[ThunkMiddlewareFor<S, O>], never>>
-
-export function curryGetDefaultMiddleware<
-  S = any
->(): CurriedGetDefaultMiddleware<S> {
-  return function curriedGetDefaultMiddleware(options) {
-    return getDefaultMiddleware(options)
-  }
-}
-
-/**
- * Returns any array containing the default middleware installed by
- * `configureStore()`. Useful if you want to configure your store with a custom
- * `middleware` array but still keep the default set.
- *
- * @return The default middleware used by `configureStore()`.
- *
- * @public
- *
- * @deprecated Prefer to use the callback notation for the `middleware` option in `configureStore`
- * to access a pre-typed `getDefaultMiddleware` instead.
- */
-export function getDefaultMiddleware<
-  S = any,
-  O extends Partial<GetDefaultMiddlewareOptions> = {
-    thunk: true
-    immutableCheck: true
-    serializableCheck: true
-    actionCreatorCheck: true
-  }
->(
-  options: O = {} as O
-): MiddlewareArray<ExcludeFromTuple<[ThunkMiddlewareFor<S, O>], never>> {
-  const {
-    thunk = true,
-    immutableCheck = true,
-    serializableCheck = true,
-    actionCreatorCheck = true,
-  } = options
-
-  let middlewareArray = new MiddlewareArray<Middleware[]>()
-
-  if (thunk) {
-    if (isBoolean(thunk)) {
-      middlewareArray.push(thunkMiddleware)
-    } else {
-      middlewareArray.push(
-        thunkMiddleware.withExtraArgument(thunk.extraArgument)
-      )
+) => Tuple<ExcludeFromTuple<[ThunkMiddlewareFor<S, O>], never>>
+
+export const buildGetDefaultMiddleware = <S = any>(): GetDefaultMiddleware<S> =>
+  function getDefaultMiddleware(options) {
+    const {
+      thunk = true,
+      immutableCheck = true,
+      serializableCheck = true,
+      actionCreatorCheck = true,
+    } = options ?? {}
+
+    let middlewareArray = new Tuple<Middleware[]>()
+
+    if (thunk) {
+      if (isBoolean(thunk)) {
+        middlewareArray.push(thunkMiddleware)
+      } else {
+        middlewareArray.push(withExtraArgument(thunk.extraArgument))
+      }
     }
-  }
 
-  if (process.env.NODE_ENV !== 'production') {
-    if (immutableCheck) {
-      /* PROD_START_REMOVE_UMD */
-      let immutableOptions: ImmutableStateInvariantMiddlewareOptions = {}
+    if (process.env.NODE_ENV !== 'production') {
+      if (immutableCheck) {
+        /* PROD_START_REMOVE_UMD */
+        let immutableOptions: ImmutableStateInvariantMiddlewareOptions = {}
+
+        if (!isBoolean(immutableCheck)) {
+          immutableOptions = immutableCheck
+        }
 
-      if (!isBoolean(immutableCheck)) {
-        immutableOptions = immutableCheck
+        middlewareArray.unshift(
+          createImmutableStateInvariantMiddleware(immutableOptions)
+        )
+        /* PROD_STOP_REMOVE_UMD */
       }
 
-      middlewareArray.unshift(
-        createImmutableStateInvariantMiddleware(immutableOptions)
-      )
-      /* PROD_STOP_REMOVE_UMD */
-    }
+      if (serializableCheck) {
+        let serializableOptions: SerializableStateInvariantMiddlewareOptions =
+          {}
 
-    if (serializableCheck) {
-      let serializableOptions: SerializableStateInvariantMiddlewareOptions = {}
+        if (!isBoolean(serializableCheck)) {
+          serializableOptions = serializableCheck
+        }
 
-      if (!isBoolean(serializableCheck)) {
-        serializableOptions = serializableCheck
+        middlewareArray.push(
+          createSerializableStateInvariantMiddleware(serializableOptions)
+        )
       }
+      if (actionCreatorCheck) {
+        let actionCreatorOptions: ActionCreatorInvariantMiddlewareOptions = {}
 
-      middlewareArray.push(
-        createSerializableStateInvariantMiddleware(serializableOptions)
-      )
-    }
-    if (actionCreatorCheck) {
-      let actionCreatorOptions: ActionCreatorInvariantMiddlewareOptions = {}
+        if (!isBoolean(actionCreatorCheck)) {
+          actionCreatorOptions = actionCreatorCheck
+        }
 
-      if (!isBoolean(actionCreatorCheck)) {
-        actionCreatorOptions = actionCreatorCheck
+        middlewareArray.unshift(
+          createActionCreatorInvariantMiddleware(actionCreatorOptions)
+        )
       }
-
-      middlewareArray.unshift(
-        createActionCreatorInvariantMiddleware(actionCreatorOptions)
-      )
     }
-  }
 
-  return middlewareArray as any
-}
+    return middlewareArray as any
+  }
diff --git a/packages/toolkit/src/immutableStateInvariantMiddleware.ts b/packages/toolkit/src/immutableStateInvariantMiddleware.ts
--- a/packages/toolkit/src/immutableStateInvariantMiddleware.ts
+++ b/packages/toolkit/src/immutableStateInvariantMiddleware.ts
@@ -3,64 +3,6 @@ import { getTimeMeasureUtils } from './utils'
 
 type EntryProcessor = (key: string, value: any) => any
 
-const isProduction: boolean = process.env.NODE_ENV === 'production'
-const prefix: string = 'Invariant failed'
-
-// Throw an error if the condition fails
-// Strip out error messages for production
-// > Not providing an inline default argument for message as the result is smaller
-function invariant(condition: any, message?: string) {
-  if (condition) {
-    return
-  }
-  // Condition not passed
-
-  // In production we strip the message but still throw
-  if (isProduction) {
-    throw new Error(prefix)
-  }
-
-  // When not in production we allow the message to pass through
-  // *This block will be removed in production builds*
-  throw new Error(`${prefix}: ${message || ''}`)
-}
-
-function stringify(
-  obj: any,
-  serializer?: EntryProcessor,
-  indent?: string | number,
-  decycler?: EntryProcessor
-): string {
-  return JSON.stringify(obj, getSerialize(serializer, decycler), indent)
-}
-
-function getSerialize(
-  serializer?: EntryProcessor,
-  decycler?: EntryProcessor
-): EntryProcessor {
-  let stack: any[] = [],
-    keys: any[] = []
-
-  if (!decycler)
-    decycler = function (_: string, value: any) {
-      if (stack[0] === value) return '[Circular ~]'
-      return (
-        '[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']'
-      )
-    }
-
-  return function (this: any, key: string, value: any) {
-    if (stack.length > 0) {
-      var thisPos = stack.indexOf(this)
-      ~thisPos ? stack.splice(thisPos + 1) : stack.push(this)
-      ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key)
-      if (~stack.indexOf(value)) value = decycler!.call(this, key, value)
-    } else stack.push(value)
-
-    return serializer == null ? value : serializer.call(this, key, value)
-  }
-}
-
 /**
  * The default `isImmutable` function.
  *
@@ -98,7 +40,7 @@ function trackProperties(
   const tracked: Partial<TrackedProperty> = { value: obj }
 
   if (!isImmutable(obj) && !checkedObjects.has(obj)) {
-    checkedObjects.add(obj);
+    checkedObjects.add(obj)
     tracked.children = {}
 
     for (const key in obj) {
@@ -205,8 +147,6 @@ export interface ImmutableStateInvariantMiddlewareOptions {
   ignoredPaths?: IgnorePaths
   /** Print a warning if checks take longer than N ms. Default: 32ms */
   warnAfter?: number
-  // @deprecated. Use ignoredPaths
-  ignore?: string[]
 }
 
 /**
@@ -223,69 +163,102 @@ export function createImmutableStateInvariantMiddleware(
 ): Middleware {
   if (process.env.NODE_ENV === 'production') {
     return () => (next) => (action) => next(action)
-  }
-
-  let {
-    isImmutable = isImmutableDefault,
-    ignoredPaths,
-    warnAfter = 32,
-    ignore,
-  } = options
+  } else {
+    function stringify(
+      obj: any,
+      serializer?: EntryProcessor,
+      indent?: string | number,
+      decycler?: EntryProcessor
+    ): string {
+      return JSON.stringify(obj, getSerialize(serializer, decycler), indent)
+    }
 
-  // Alias ignore->ignoredPaths, but prefer ignoredPaths if present
-  ignoredPaths = ignoredPaths || ignore
+    function getSerialize(
+      serializer?: EntryProcessor,
+      decycler?: EntryProcessor
+    ): EntryProcessor {
+      let stack: any[] = [],
+        keys: any[] = []
+
+      if (!decycler)
+        decycler = function (_: string, value: any) {
+          if (stack[0] === value) return '[Circular ~]'
+          return (
+            '[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']'
+          )
+        }
 
-  const track = trackForMutations.bind(null, isImmutable, ignoredPaths)
+      return function (this: any, key: string, value: any) {
+        if (stack.length > 0) {
+          var thisPos = stack.indexOf(this)
+          ~thisPos ? stack.splice(thisPos + 1) : stack.push(this)
+          ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key)
+          if (~stack.indexOf(value)) value = decycler!.call(this, key, value)
+        } else stack.push(value)
 
-  return ({ getState }) => {
-    let state = getState()
-    let tracker = track(state)
+        return serializer == null ? value : serializer.call(this, key, value)
+      }
+    }
 
-    let result
-    return (next) => (action) => {
-      const measureUtils = getTimeMeasureUtils(
-        warnAfter,
-        'ImmutableStateInvariantMiddleware'
-      )
+    let {
+      isImmutable = isImmutableDefault,
+      ignoredPaths,
+      warnAfter = 32,
+    } = options
 
-      measureUtils.measureTime(() => {
-        state = getState()
+    const track = trackForMutations.bind(null, isImmutable, ignoredPaths)
 
-        result = tracker.detectMutations()
-        // Track before potentially not meeting the invariant
-        tracker = track(state)
+    return ({ getState }) => {
+      let state = getState()
+      let tracker = track(state)
 
-        invariant(
-          !result.wasMutated,
-          `A state mutation was detected between dispatches, in the path '${
-            result.path || ''
-          }'.  This may cause incorrect behavior. (https://redux.js.org/style-guide/style-guide#do-not-mutate-state)`
+      let result
+      return (next) => (action) => {
+        const measureUtils = getTimeMeasureUtils(
+          warnAfter,
+          'ImmutableStateInvariantMiddleware'
         )
-      })
 
-      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 || ''
-            }. Take a look at the reducer(s) handling the action ${stringify(
-              action
-            )}. (https://redux.js.org/style-guide/style-guide#do-not-mutate-state)`
-          )
-      })
-
-      measureUtils.warnIfExceeded()
-
-      return dispatchedAction
+        measureUtils.measureTime(() => {
+          state = getState()
+
+          result = tracker.detectMutations()
+          // Track before potentially not meeting the invariant
+          tracker = track(state)
+
+          if (result.wasMutated) {
+            throw new Error(
+              `A state mutation was detected between dispatches, in the path '${
+                result.path || ''
+              }'.  This may cause incorrect behavior. (https://redux.js.org/style-guide/style-guide#do-not-mutate-state)`
+            )
+          }
+        })
+
+        const dispatchedAction = next(action)
+
+        measureUtils.measureTime(() => {
+          state = getState()
+
+          result = tracker.detectMutations()
+          // Track before potentially not meeting the invariant
+          tracker = track(state)
+
+          if (result.wasMutated) {
+            throw new Error(
+              `A state mutation was detected inside a dispatch, in the path: ${
+                result.path || ''
+              }. Take a look at the reducer(s) handling the action ${stringify(
+                action
+              )}. (https://redux.js.org/style-guide/style-guide#do-not-mutate-state)`
+            )
+          }
+        })
+
+        measureUtils.warnIfExceeded()
+
+        return dispatchedAction
+      }
     }
   }
 }
diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts
--- a/packages/toolkit/src/index.ts
+++ b/packages/toolkit/src/index.ts
@@ -1,36 +1,35 @@
-import { enableES5 } from 'immer'
+// This must remain here so that the `mangleErrors.cjs` build script
+// does not have to import this into each source file it rewrites.
+import { formatProdErrorMessage } from './formatProdErrorMessage'
+
 export * from 'redux'
 export {
-  default as createNextState,
+  produce as createNextState,
   current,
   freeze,
   original,
   isDraft,
 } from 'immer'
 export type { Draft } from 'immer'
-export { createSelector } from 'reselect'
-export type {
-  Selector,
-  OutputParametricSelector,
-  OutputSelector,
-  ParametricSelector,
+export {
+  createSelector,
+  createSelectorCreator,
+  lruMemoize,
+  weakMapMemoize,
 } from 'reselect'
-export { createDraftSafeSelector } from './createDraftSafeSelector'
+export type { Selector, OutputSelector } from 'reselect'
+export {
+  createDraftSafeSelector,
+  createDraftSafeSelectorCreator,
+} from './createDraftSafeSelector'
 export type { ThunkAction, ThunkDispatch, ThunkMiddleware } from 'redux-thunk'
 
-// We deliberately enable Immer's ES5 support, on the grounds that
-// we assume RTK will be used with React Native and other Proxy-less
-// environments.  In addition, that's how Immer 4 behaved, and since
-// we want to ship this in an RTK minor, we should keep the same behavior.
-enableES5()
-
 export {
   // js
   configureStore,
 } from './configureStore'
 export type {
   // types
-  ConfigureEnhancersCallback,
   ConfigureStoreOptions,
   EnhancedStore,
 } from './configureStore'
@@ -38,8 +37,6 @@ export type { DevToolsEnhancerOptions } from './devtoolsExtension'
 export {
   // js
   createAction,
-  getType,
-  isAction,
   isActionCreator,
   isFSA as isFluxStandardAction,
 } from './createAction'
@@ -67,6 +64,9 @@ export type {
 export {
   // js
   createSlice,
+  buildCreateSlice,
+  asyncThunkCreator,
+  ReducerType,
 } from './createSlice'
 
 export type {
@@ -77,7 +77,8 @@ export type {
   SliceCaseReducers,
   ValidateSliceCaseReducers,
   CaseReducerWithPrepare,
-  SliceActionCreator,
+  ReducerCreators,
+  SliceSelectors,
 } from './createSlice'
 export type { ActionCreatorInvariantMiddlewareOptions } from './actionCreatorInvariantMiddleware'
 export { createActionCreatorInvariantMiddleware } from './actionCreatorInvariantMiddleware'
@@ -100,19 +101,14 @@ export type {
   // types
   SerializableStateInvariantMiddlewareOptions,
 } from './serializableStateInvariantMiddleware'
-export {
-  // js
-  getDefaultMiddleware,
-} from './getDefaultMiddleware'
 export type {
   // types
   ActionReducerMapBuilder,
 } from './mapBuilders'
-export { MiddlewareArray, EnhancerArray } from './utils'
+export { Tuple } from './utils'
 
 export { createEntityAdapter } from './entities/create_adapter'
 export type {
-  Dictionary,
   EntityState,
   EntityAdapter,
   EntitySelectors,
@@ -155,8 +151,6 @@ export type {
 
 export { nanoid } from './nanoid'
 
-export { default as isPlainObject } from './isPlainObject'
-
 export type {
   ListenerEffect,
   ListenerMiddleware,
@@ -190,6 +184,14 @@ export {
   TaskAbortError,
 } from './listenerMiddleware/index'
 
+export type {
+  DynamicMiddlewareInstance,
+  GetDispatch,
+  GetState,
+  MiddlewareApiConfig,
+} from './dynamicMiddleware/types'
+export { createDynamicMiddleware } from './dynamicMiddleware/index'
+
 export {
   SHOULD_AUTOBATCH,
   prepareAutoBatched,
@@ -197,4 +199,10 @@ export {
 } from './autoBatchEnhancer'
 export type { AutoBatchOptions } from './autoBatchEnhancer'
 
+export { combineSlices } from './combineSlices'
+
+export type { WithSlice } from './combineSlices'
+
 export type { ExtractDispatchExtensions as TSHelpersExtractDispatchExtensions } from './tsHelpers'
+
+export { formatProdErrorMessage } from './formatProdErrorMessage'
diff --git a/packages/toolkit/src/isPlainObject.ts b/packages/toolkit/src/isPlainObject.ts
deleted file mode 100644
--- a/packages/toolkit/src/isPlainObject.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Returns true if the passed value is "plain" object, i.e. an object whose
- * prototype is the root `Object.prototype`. This includes objects created
- * using object literals, but not for instance for class instances.
- *
- * @param {any} value The value to inspect.
- * @returns {boolean} True if the argument appears to be a plain object.
- *
- * @public
- */
-export default function isPlainObject(value: unknown): value is object {
-  if (typeof value !== 'object' || value === null) return false
-
-  let proto = Object.getPrototypeOf(value)
-  if (proto === null) return true
-
-  let baseProto = proto
-  while (Object.getPrototypeOf(baseProto) !== null) {
-    baseProto = Object.getPrototypeOf(baseProto)
-  }
-
-  return proto === baseProto
-}
diff --git a/packages/toolkit/src/listenerMiddleware/index.ts b/packages/toolkit/src/listenerMiddleware/index.ts
--- a/packages/toolkit/src/listenerMiddleware/index.ts
+++ b/packages/toolkit/src/listenerMiddleware/index.ts
@@ -1,6 +1,7 @@
-import type { Dispatch, AnyAction, MiddlewareAPI } from 'redux'
+import type { Action, Dispatch, MiddlewareAPI, UnknownAction } from 'redux'
+import { isAction } from 'redux'
 import type { ThunkDispatch } from 'redux-thunk'
-import { createAction, isAction } from '../createAction'
+import { createAction } from '../createAction'
 import { nanoid } from '../nanoid'
 
 import type {
@@ -45,6 +46,7 @@ import {
   createDelay,
   raceWithSignal,
 } from './task'
+import { find } from '../utils'
 export { TaskAbortError } from './exceptions'
 export type {
   ListenerEffect,
@@ -126,11 +128,7 @@ const createFork = (
 }
 
 const createTakePattern = <S>(
-  startListening: AddListenerOverloads<
-    UnsubscribeListener,
-    S,
-    Dispatch<AnyAction>
-  >,
+  startListening: AddListenerOverloads<UnsubscribeListener, S, Dispatch>,
   signal: AbortSignal
 ): TakePattern<S> => {
   /**
@@ -149,7 +147,7 @@ const createTakePattern = <S>(
     // Placeholder unsubscribe function until the listener is added
     let unsubscribe: UnsubscribeListener = () => {}
 
-    const tuplePromise = new Promise<[AnyAction, S, S]>((resolve, reject) => {
+    const tuplePromise = new Promise<[Action, S, S]>((resolve, reject) => {
       // Inside the Promise, we synchronously add the listener.
       let stopListening = startListening({
         predicate: predicate as any,
@@ -170,9 +168,7 @@ const createTakePattern = <S>(
       }
     })
 
-    const promises: (Promise<null> | Promise<[AnyAction, S, S]>)[] = [
-      tuplePromise,
-    ]
+    const promises: (Promise<null> | Promise<[Action, S, S]>)[] = [tuplePromise]
 
     if (timeout != null) {
       promises.push(
@@ -240,7 +236,7 @@ export const createListenerEntry: TypedCreateListenerEntry<unknown> = (
 }
 
 const cancelActiveListeners = (
-  entry: ListenerEntry<unknown, Dispatch<AnyAction>>
+  entry: ListenerEntry<unknown, Dispatch<UnknownAction>>
 ) => {
   entry.pending.forEach((controller) => {
     abortControllerWithReason(controller, listenerCancelled)
@@ -308,7 +304,7 @@ const defaultErrorHandler: ListenerErrorHandler = (...args: unknown[]) => {
  */
 export function createListenerMiddleware<
   S = unknown,
-  D extends Dispatch<AnyAction> = ThunkDispatch<S, unknown, AnyAction>,
+  D extends Dispatch<Action> = ThunkDispatch<S, unknown, UnknownAction>,
   ExtraArgument = unknown
 >(middlewareOptions: CreateListenerMiddlewareOptions<ExtraArgument> = {}) {
   const listenerMap = new Map<string, ListenerEntry>()
@@ -328,20 +324,9 @@ export function createListenerMiddleware<
     }
   }
 
-  const findListenerEntry = (
-    comparator: (entry: ListenerEntry) => boolean
-  ): ListenerEntry | undefined => {
-    for (const entry of Array.from(listenerMap.values())) {
-      if (comparator(entry)) {
-        return entry
-      }
-    }
-
-    return undefined
-  }
-
   const startListening = (options: FallbackAddListenerOptions) => {
-    let entry = findListenerEntry(
+    let entry = find(
+      Array.from(listenerMap.values()),
       (existingEntry) => existingEntry.effect === options.effect
     )
 
@@ -357,7 +342,7 @@ export function createListenerMiddleware<
   ): boolean => {
     const { type, effect, predicate } = getListenerEntryPropsFrom(options)
 
-    const entry = findListenerEntry((entry) => {
+    const entry = find(Array.from(listenerMap.values()), (entry) => {
       const matchPredicateOrType =
         typeof type === 'string'
           ? entry.type === type
@@ -377,8 +362,8 @@ export function createListenerMiddleware<
   }
 
   const notifyListener = async (
-    entry: ListenerEntry<unknown, Dispatch<AnyAction>>,
-    action: AnyAction,
+    entry: ListenerEntry<unknown, Dispatch<UnknownAction>>,
+    action: unknown,
     api: MiddlewareAPI,
     getOriginalState: () => S
   ) => {
diff --git a/packages/toolkit/src/listenerMiddleware/task.ts b/packages/toolkit/src/listenerMiddleware/task.ts
--- a/packages/toolkit/src/listenerMiddleware/task.ts
+++ b/packages/toolkit/src/listenerMiddleware/task.ts
@@ -10,7 +10,8 @@ import { addAbortSignalListener, catchRejection, noop } from './utils'
  */
 export const validateActive = (signal: AbortSignal): void => {
   if (signal.aborted) {
-    throw new TaskAbortError((signal as AbortSignalWithReason<string>).reason)
+    const { reason } = signal as AbortSignalWithReason<string>
+    throw new TaskAbortError(reason)
   }
 }
 
diff --git a/packages/toolkit/src/listenerMiddleware/types.ts b/packages/toolkit/src/listenerMiddleware/types.ts
--- a/packages/toolkit/src/listenerMiddleware/types.ts
+++ b/packages/toolkit/src/listenerMiddleware/types.ts
@@ -1,13 +1,14 @@
 import type { PayloadAction, BaseActionCreator } from '../createAction'
 import type {
   Dispatch as ReduxDispatch,
-  AnyAction,
   MiddlewareAPI,
   Middleware,
   Action as ReduxAction,
+  UnknownAction,
 } from 'redux'
 import type { ThunkDispatch } from 'redux-thunk'
 import type { TaskAbortError } from './exceptions'
+import { NoInfer } from '../tsHelpers'
 
 /**
  * @internal
@@ -28,14 +29,14 @@ export interface TypedActionCreator<Type extends string> {
 
 /** @internal */
 export type AnyListenerPredicate<State> = (
-  action: AnyAction,
+  action: UnknownAction,
   currentState: State,
   originalState: State
 ) => boolean
 
 /** @public */
-export type ListenerPredicate<Action extends AnyAction, State> = (
-  action: AnyAction,
+export type ListenerPredicate<Action extends ReduxAction, State> = (
+  action: UnknownAction,
   currentState: State,
   originalState: State
 ) => action is Action
@@ -143,7 +144,7 @@ export interface ForkOptions {
 /** @public */
 export interface ListenerEffectAPI<
   State,
-  Dispatch extends ReduxDispatch<AnyAction>,
+  Dispatch extends ReduxDispatch,
   ExtraArgument = unknown
 > extends MiddlewareAPI<Dispatch, State> {
   /**
@@ -269,9 +270,9 @@ export interface ListenerEffectAPI<
 
 /** @public */
 export type ListenerEffect<
-  Action extends AnyAction,
+  Action extends ReduxAction,
   State,
-  Dispatch extends ReduxDispatch<AnyAction>,
+  Dispatch extends ReduxDispatch,
   ExtraArgument = unknown
 > = (
   action: Action,
@@ -311,10 +312,10 @@ export interface CreateListenerMiddlewareOptions<ExtraArgument = unknown> {
 /** @public */
 export type ListenerMiddleware<
   State = unknown,
-  Dispatch extends ThunkDispatch<State, unknown, AnyAction> = ThunkDispatch<
+  Dispatch extends ThunkDispatch<State, unknown, ReduxAction> = ThunkDispatch<
     State,
     unknown,
-    AnyAction
+    UnknownAction
   >,
   ExtraArgument = unknown
 > = Middleware<
@@ -328,10 +329,10 @@ export type ListenerMiddleware<
 /** @public */
 export interface ListenerMiddlewareInstance<
   State = unknown,
-  Dispatch extends ThunkDispatch<State, unknown, AnyAction> = ThunkDispatch<
+  Dispatch extends ThunkDispatch<State, unknown, ReduxAction> = ThunkDispatch<
     State,
     unknown,
-    AnyAction
+    UnknownAction
   >,
   ExtraArgument = unknown
 > {
@@ -359,7 +360,7 @@ export type TakePatternOutputWithoutTimeout<
   Predicate extends AnyListenerPredicate<State>
 > = Predicate extends MatchFunction<infer Action>
   ? Promise<[Action, State, State]>
-  : Promise<[AnyAction, State, State]>
+  : Promise<[UnknownAction, State, State]>
 
 /** @public */
 export type TakePatternOutputWithTimeout<
@@ -367,7 +368,7 @@ export type TakePatternOutputWithTimeout<
   Predicate extends AnyListenerPredicate<State>
 > = Predicate extends MatchFunction<infer Action>
   ? Promise<[Action, State, State] | null>
-  : Promise<[AnyAction, State, State] | null>
+  : Promise<[UnknownAction, State, State] | null>
 
 /** @public */
 export interface TakePattern<State> {
@@ -401,12 +402,12 @@ export type UnsubscribeListener = (
 export interface AddListenerOverloads<
   Return,
   State = unknown,
-  Dispatch extends ReduxDispatch = ThunkDispatch<State, unknown, AnyAction>,
+  Dispatch extends ReduxDispatch = ThunkDispatch<State, unknown, UnknownAction>,
   ExtraArgument = unknown,
   AdditionalOptions = unknown
 > {
   /** Accepts a "listener predicate" that is also a TS type predicate for the action*/
-  <MA extends AnyAction, LP extends ListenerPredicate<MA, State>>(
+  <MA extends UnknownAction, LP extends ListenerPredicate<MA, State>>(
     options: {
       actionCreator?: never
       type?: never
@@ -444,7 +445,7 @@ export interface AddListenerOverloads<
   ): Return
 
   /** Accepts an RTK matcher function, such as `incrementByAmount.match` */
-  <MA extends AnyAction, M extends MatchFunction<MA>>(
+  <M extends MatchFunction<UnknownAction>>(
     options: {
       actionCreator?: never
       type?: never
@@ -461,7 +462,7 @@ export interface AddListenerOverloads<
       type?: never
       matcher?: never
       predicate: LP
-      effect: ListenerEffect<AnyAction, State, Dispatch, ExtraArgument>
+      effect: ListenerEffect<UnknownAction, State, Dispatch, ExtraArgument>
     } & AdditionalOptions
   ): Return
 }
@@ -469,7 +470,7 @@ export interface AddListenerOverloads<
 /** @public */
 export type RemoveListenerOverloads<
   State = unknown,
-  Dispatch extends ReduxDispatch = ThunkDispatch<State, unknown, AnyAction>
+  Dispatch extends ReduxDispatch = ThunkDispatch<State, unknown, UnknownAction>
 > = AddListenerOverloads<
   boolean,
   State,
@@ -480,9 +481,9 @@ export type RemoveListenerOverloads<
 
 /** @public */
 export interface RemoveListenerAction<
-  Action extends AnyAction,
+  Action extends UnknownAction,
   State,
-  Dispatch extends ReduxDispatch<AnyAction>
+  Dispatch extends ReduxDispatch
 > {
   type: 'listenerMiddleware/remove'
   payload: {
@@ -496,11 +497,7 @@ export interface RemoveListenerAction<
  * A "pre-typed" version of `addListenerAction`, so the listener args are well-typed */
 export type TypedAddListener<
   State,
-  Dispatch extends ReduxDispatch<AnyAction> = ThunkDispatch<
-    State,
-    unknown,
-    AnyAction
-  >,
+  Dispatch extends ReduxDispatch = ThunkDispatch<State, unknown, UnknownAction>,
   ExtraArgument = unknown,
   Payload = ListenerEntry<State, Dispatch>,
   T extends string = 'listenerMiddleware/add'
@@ -517,11 +514,7 @@ export type TypedAddListener<
  * A "pre-typed" version of `removeListenerAction`, so the listener args are well-typed */
 export type TypedRemoveListener<
   State,
-  Dispatch extends ReduxDispatch<AnyAction> = ThunkDispatch<
-    State,
-    unknown,
-    AnyAction
-  >,
+  Dispatch extends ReduxDispatch = ThunkDispatch<State, unknown, UnknownAction>,
   Payload = ListenerEntry<State, Dispatch>,
   T extends string = 'listenerMiddleware/remove'
 > = BaseActionCreator<Payload, T> &
@@ -538,11 +531,7 @@ export type TypedRemoveListener<
  * A "pre-typed" version of `middleware.startListening`, so the listener args are well-typed */
 export type TypedStartListening<
   State,
-  Dispatch extends ReduxDispatch<AnyAction> = ThunkDispatch<
-    State,
-    unknown,
-    AnyAction
-  >,
+  Dispatch extends ReduxDispatch = ThunkDispatch<State, unknown, UnknownAction>,
   ExtraArgument = unknown
 > = AddListenerOverloads<UnsubscribeListener, State, Dispatch, ExtraArgument>
 
@@ -550,22 +539,14 @@ export type TypedStartListening<
  * A "pre-typed" version of `middleware.stopListening`, so the listener args are well-typed */
 export type TypedStopListening<
   State,
-  Dispatch extends ReduxDispatch<AnyAction> = ThunkDispatch<
-    State,
-    unknown,
-    AnyAction
-  >
+  Dispatch extends ReduxDispatch = ThunkDispatch<State, unknown, UnknownAction>
 > = RemoveListenerOverloads<State, Dispatch>
 
 /** @public
  * A "pre-typed" version of `createListenerEntry`, so the listener args are well-typed */
 export type TypedCreateListenerEntry<
   State,
-  Dispatch extends ReduxDispatch<AnyAction> = ThunkDispatch<
-    State,
-    unknown,
-    AnyAction
-  >
+  Dispatch extends ReduxDispatch = ThunkDispatch<State, unknown, UnknownAction>
 > = AddListenerOverloads<ListenerEntry<State, Dispatch>, State, Dispatch>
 
 /**
@@ -575,14 +556,14 @@ export type TypedCreateListenerEntry<
 /** @internal An single listener entry */
 export type ListenerEntry<
   State = unknown,
-  Dispatch extends ReduxDispatch<AnyAction> = ReduxDispatch<AnyAction>
+  Dispatch extends ReduxDispatch = ReduxDispatch
 > = {
   id: string
   effect: ListenerEffect<any, State, Dispatch>
   unsubscribe: () => void
   pending: Set<AbortController>
   type?: string
-  predicate: ListenerPredicate<AnyAction, State>
+  predicate: ListenerPredicate<UnknownAction, State>
 }
 
 /**
@@ -601,10 +582,7 @@ export type FallbackAddListenerOptions = {
  */
 
 /** @public */
-export type GuardedType<T> = T extends (
-  x: any,
-  ...args: unknown[]
-) => x is infer T
+export type GuardedType<T> = T extends (x: any, ...args: any[]) => x is infer T
   ? T
   : never
 
diff --git a/packages/toolkit/src/mapBuilders.ts b/packages/toolkit/src/mapBuilders.ts
--- a/packages/toolkit/src/mapBuilders.ts
+++ b/packages/toolkit/src/mapBuilders.ts
@@ -1,4 +1,4 @@
-import type { Action, AnyAction } from 'redux'
+import type { Action } from 'redux'
 import type {
   CaseReducer,
   CaseReducers,
@@ -56,7 +56,7 @@ import {
   createAction,
   createReducer,
   AsyncThunk,
-  AnyAction,
+  UnknownAction,
 } from "@reduxjs/toolkit";
 
 type GenericAsyncThunk = AsyncThunk<unknown, unknown, any>;
@@ -68,8 +68,8 @@ type FulfilledAction = ReturnType<GenericAsyncThunk["fulfilled"]>;
 const initialState: Record<string, string> = {};
 const resetAction = createAction("reset-tracked-loading-state");
 
-function isPendingAction(action: AnyAction): action is PendingAction {
-  return action.type.endsWith("/pending");
+function isPendingAction(action: UnknownAction): action is PendingAction {
+  return typeof action.type === "string" && action.type.endsWith("/pending");
 }
 
 const reducer = createReducer(initialState, (builder) => {
@@ -98,7 +98,7 @@ const reducer = createReducer(initialState, (builder) => {
    */
   addMatcher<A>(
     matcher: TypeGuard<A> | ((action: any) => boolean),
-    reducer: CaseReducer<State, A extends AnyAction ? A : A & AnyAction>
+    reducer: CaseReducer<State, A extends Action ? A : A & Action>
   ): Omit<ActionReducerMapBuilder<State>, 'addCase'>
 
   /**
@@ -120,7 +120,7 @@ const reducer = createReducer(initialState, builder => {
 })
 ```
    */
-  addDefaultCase(reducer: CaseReducer<State, AnyAction>): {}
+  addDefaultCase(reducer: CaseReducer<State, Action>): {}
 }
 
 export function executeReducerBuilderCallback<S>(
@@ -128,11 +128,11 @@ export function executeReducerBuilderCallback<S>(
 ): [
   CaseReducers<S, any>,
   ActionMatcherDescriptionCollection<S>,
-  CaseReducer<S, AnyAction> | undefined
+  CaseReducer<S, Action> | undefined
 ] {
   const actionsMap: CaseReducers<S, any> = {}
   const actionMatchers: ActionMatcherDescriptionCollection<S> = []
-  let defaultCaseReducer: CaseReducer<S, AnyAction> | undefined
+  let defaultCaseReducer: CaseReducer<S, Action> | undefined
   const builder = {
     addCase(
       typeOrActionCreator: string | TypedActionCreator<any>,
@@ -166,7 +166,8 @@ export function executeReducerBuilderCallback<S>(
       }
       if (type in actionsMap) {
         throw new Error(
-          `\`builder.addCase\` cannot be called with two reducers for the same action type '${type}'`
+          '`builder.addCase` cannot be called with two reducers for the same action type ' +
+            `'${type}'`
         )
       }
       actionsMap[type] = reducer
@@ -174,7 +175,7 @@ export function executeReducerBuilderCallback<S>(
     },
     addMatcher<A>(
       matcher: TypeGuard<A>,
-      reducer: CaseReducer<S, A extends AnyAction ? A : A & AnyAction>
+      reducer: CaseReducer<S, A extends Action ? A : A & Action>
     ) {
       if (process.env.NODE_ENV !== 'production') {
         if (defaultCaseReducer) {
@@ -186,7 +187,7 @@ export function executeReducerBuilderCallback<S>(
       actionMatchers.push({ matcher, reducer })
       return builder
     },
-    addDefaultCase(reducer: CaseReducer<S, AnyAction>) {
+    addDefaultCase(reducer: CaseReducer<S, Action>) {
       if (process.env.NODE_ENV !== 'production') {
         if (defaultCaseReducer) {
           throw new Error('`builder.addDefaultCase` can only be called once')
diff --git a/packages/toolkit/src/query/apiTypes.ts b/packages/toolkit/src/query/apiTypes.ts
--- a/packages/toolkit/src/query/apiTypes.ts
+++ b/packages/toolkit/src/query/apiTypes.ts
@@ -13,7 +13,7 @@ import type { CoreModule } from './core/module'
 import type { CreateApiOptions } from './createApi'
 import type { BaseQueryFn } from './baseQueryTypes'
 import type { CombinedState } from './core/apiState'
-import type { AnyAction } from '@reduxjs/toolkit'
+import type { UnknownAction } from '@reduxjs/toolkit'
 
 export interface ApiModules<
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -45,6 +45,7 @@ export type Module<Name extends ModuleName> = {
       | 'refetchOnMountOrArgChange'
       | 'refetchOnFocus'
       | 'refetchOnReconnect'
+      | 'invalidationBehavior'
       | 'tagTypes'
     >,
     context: ApiContext<Definitions>
@@ -61,9 +62,9 @@ export interface ApiContext<Definitions extends EndpointDefinitions> {
   endpointDefinitions: Definitions
   batch(cb: () => void): void
   extractRehydrationInfo: (
-    action: AnyAction
+    action: UnknownAction
   ) => CombinedState<any, any, any> | undefined
-  hasRehydrationInfo: (action: AnyAction) => boolean
+  hasRehydrationInfo: (action: UnknownAction) => boolean
 }
 
 export type Api<
diff --git a/packages/toolkit/src/query/core/apiState.ts b/packages/toolkit/src/query/core/apiState.ts
--- a/packages/toolkit/src/query/core/apiState.ts
+++ b/packages/toolkit/src/query/core/apiState.ts
@@ -254,6 +254,7 @@ export type ConfigState<ReducerPath> = RefetchConfigOptions & {
 
 export type ModifiableConfigState = {
   keepUnusedDataFor: number
+  invalidationBehavior: 'delayed' | 'immediately'
 } & RefetchConfigOptions
 
 export type MutationState<D extends EndpointDefinitions> = {
diff --git a/packages/toolkit/src/query/core/buildInitiate.ts b/packages/toolkit/src/query/core/buildInitiate.ts
--- a/packages/toolkit/src/query/core/buildInitiate.ts
+++ b/packages/toolkit/src/query/core/buildInitiate.ts
@@ -7,7 +7,11 @@ import type {
 } from '../endpointDefinitions'
 import { DefinitionType, isQueryDefinition } from '../endpointDefinitions'
 import type { QueryThunk, MutationThunk, QueryThunkArg } from './buildThunks'
-import type { AnyAction, ThunkAction, SerializedError } from '@reduxjs/toolkit'
+import type {
+  UnknownAction,
+  ThunkAction,
+  SerializedError,
+} from '@reduxjs/toolkit'
 import type { SubscriptionOptions, RootState } from './apiState'
 import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
 import type { Api, ApiContext } from '../apiTypes'
@@ -16,6 +20,7 @@ import type { BaseQueryError, QueryReturnValue } from '../baseQueryTypes'
 import type { QueryResultSelectorResult } from './buildSelectors'
 import type { Dispatch } from 'redux'
 import { isNotNullish } from '../utils/isNotNullish'
+import { countObjectKeys } from '../utils/countObjectKeys'
 
 declare module './module' {
   export interface ApiEndpointQuery<
@@ -51,7 +56,7 @@ type StartQueryActionCreator<
 > = (
   arg: QueryArgFrom<D>,
   options?: StartQueryActionCreatorOptions
-) => ThunkAction<QueryActionCreatorResult<D>, any, any, AnyAction>
+) => ThunkAction<QueryActionCreatorResult<D>, any, any, UnknownAction>
 
 export type QueryActionCreatorResult<
   D extends QueryDefinition<any, any, any, any>
@@ -81,7 +86,7 @@ type StartMutationActionCreator<
     track?: boolean
     fixedCacheKey?: string
   }
-) => ThunkAction<MutationActionCreatorResult<D>, any, any, AnyAction>
+) => ThunkAction<MutationActionCreatorResult<D>, any, any, UnknownAction>
 
 export type MutationActionCreatorResult<
   D extends MutationDefinition<any, any, any, any>
@@ -181,8 +186,6 @@ export type MutationActionCreatorResult<
    The value returned by the hook will reset to `isUninitialized` afterwards.
    */
   reset(): void
-  /** @deprecated has been renamed to `reset` */
-  unsubscribe(): void
 }
 
 export function buildInitiate({
@@ -219,37 +222,6 @@ export function buildInitiate({
     getRunningMutationThunk,
     getRunningQueriesThunk,
     getRunningMutationsThunk,
-    getRunningOperationPromises,
-    removalWarning,
-  }
-
-  /** @deprecated to be removed in 2.0 */
-  function removalWarning(): never {
-    throw new Error(
-      `This method had to be removed due to a conceptual bug in RTK.
-       Please see https://github.com/reduxjs/redux-toolkit/pull/2481 for details.
-       See https://redux-toolkit.js.org/rtk-query/usage/server-side-rendering for new guidance on SSR.`
-    )
-  }
-
-  /** @deprecated to be removed in 2.0 */
-  function getRunningOperationPromises() {
-    if (
-      typeof process !== 'undefined' &&
-      process.env.NODE_ENV === 'development'
-    ) {
-      removalWarning()
-    } else {
-      const extract = <T>(
-        v: Map<Dispatch<AnyAction>, Record<string, T | undefined>>
-      ) =>
-        Array.from(v.values()).flatMap((queriesForStore) =>
-          queriesForStore ? Object.values(queriesForStore) : []
-        )
-      return [...extract(runningQueries), ...extract(runningMutations)].filter(
-        isNotNullish
-      )
-    }
   }
 
   function getRunningQueryThunk(endpointName: string, queryArgs: any) {
@@ -294,19 +266,18 @@ export function buildInitiate({
   function middlewareWarning(dispatch: Dispatch) {
     if (process.env.NODE_ENV !== 'production') {
       if ((middlewareWarning as any).triggered) return
-      const registered:
-        | ReturnType<typeof api.internalActions.internal_probeSubscription>
-        | boolean = dispatch(
-        api.internalActions.internal_probeSubscription({
-          queryCacheKey: 'DOES_NOT_EXIST',
-          requestId: 'DUMMY_REQUEST_ID',
-        })
+      const returnedValue = dispatch(
+        api.internalActions.internal_getRTKQSubscriptions()
       )
 
       ;(middlewareWarning as any).triggered = true
 
-      // The RTKQ middleware _should_ always return a boolean for `probeSubscription`
-      if (typeof registered !== 'boolean') {
+      // The RTKQ middleware should return the internal state object,
+      // but it should _not_ be the action object.
+      if (
+        typeof returnedValue !== 'object' ||
+        typeof returnedValue?.type === 'string'
+      ) {
         // Otherwise, must not have been added
         throw new Error(
           `Warning: Middleware for RTK-Query API at reducerPath "${api.reducerPath}" has not been added to the store.
@@ -424,7 +395,7 @@ You must add the middleware for RTK-Query to function correctly!`
 
           statePromise.then(() => {
             delete running[queryCacheKey]
-            if (!Object.keys(running).length) {
+            if (!countObjectKeys(running)) {
               runningQueries.delete(dispatch)
             }
           })
@@ -464,7 +435,6 @@ You must add the middleware for RTK-Query to function correctly!`
           requestId,
           abort,
           unwrap,
-          unsubscribe: reset,
           reset,
         })
 
@@ -473,7 +443,7 @@ You must add the middleware for RTK-Query to function correctly!`
         running[requestId] = ret
         ret.then(() => {
           delete running[requestId]
-          if (!Object.keys(running).length) {
+          if (!countObjectKeys(running)) {
             runningMutations.delete(dispatch)
           }
         })
@@ -482,7 +452,7 @@ You must add the middleware for RTK-Query to function correctly!`
           ret.then(() => {
             if (running[fixedCacheKey] === ret) {
               delete running[fixedCacheKey]
-              if (!Object.keys(running).length) {
+              if (!countObjectKeys(running)) {
                 runningMutations.delete(dispatch)
               }
             }
diff --git a/packages/toolkit/src/query/core/buildMiddleware/batchActions.ts b/packages/toolkit/src/query/core/buildMiddleware/batchActions.ts
--- a/packages/toolkit/src/query/core/buildMiddleware/batchActions.ts
+++ b/packages/toolkit/src/query/core/buildMiddleware/batchActions.ts
@@ -1,42 +1,18 @@
-import type { QueryThunk, RejectedAction } from '../buildThunks'
-import type { InternalHandlerBuilder } from './types'
-import type {
-  SubscriptionState,
-  QuerySubstateIdentifier,
-  Subscribers,
-} from '../apiState'
+import type { InternalHandlerBuilder, SubscriptionSelectors } from './types'
+import type { SubscriptionState } from '../apiState'
 import { produceWithPatches } from 'immer'
-import type { AnyAction } from '@reduxjs/toolkit';
-import { createSlice, PayloadAction } from '@reduxjs/toolkit'
-
-// Copied from https://github.com/feross/queue-microtask
-let promise: Promise<any>
-const queueMicrotaskShim =
-  typeof queueMicrotask === 'function'
-    ? queueMicrotask.bind(
-        typeof window !== 'undefined'
-          ? window
-          : typeof global !== 'undefined'
-          ? global
-          : globalThis
-      )
-    : // reuse resolved promise, and allocate it lazily
-      (cb: () => void) =>
-        (promise || (promise = Promise.resolve())).then(cb).catch((err: any) =>
-          setTimeout(() => {
-            throw err
-          }, 0)
-        )
+import type { Action } from '@reduxjs/toolkit'
+import { countObjectKeys } from '../../utils/countObjectKeys'
 
 export const buildBatchedActionsHandler: InternalHandlerBuilder<
-  [actionShouldContinue: boolean, subscriptionExists: boolean]
+  [actionShouldContinue: boolean, returnValue: SubscriptionSelectors | boolean]
 > = ({ api, queryThunk, internalState }) => {
   const subscriptionsPrefix = `${api.reducerPath}/subscriptions`
 
   let previousSubscriptions: SubscriptionState =
     null as unknown as SubscriptionState
 
-  let dispatchQueued = false
+  let updateSyncTimer: ReturnType<typeof window.setTimeout> | null = null
 
   const { updateSubscriptionOptions, unsubscribeQueryResult } =
     api.internalActions
@@ -45,7 +21,7 @@ export const buildBatchedActionsHandler: InternalHandlerBuilder<
   // This is done to speed up perf when loading many components
   const actuallyMutateSubscriptions = (
     mutableState: SubscriptionState,
-    action: AnyAction
+    action: Action
   ) => {
     if (updateSubscriptionOptions.match(action)) {
       const { queryCacheKey, requestId, options } = action.payload
@@ -70,13 +46,23 @@ export const buildBatchedActionsHandler: InternalHandlerBuilder<
       const {
         meta: { arg, requestId },
       } = action
+      const substate = (mutableState[arg.queryCacheKey] ??= {})
+      substate[`${requestId}_running`] = {}
       if (arg.subscribe) {
-        const substate = (mutableState[arg.queryCacheKey] ??= {})
         substate[requestId] =
           arg.subscriptionOptions ?? substate[requestId] ?? {}
-
-        return true
       }
+      return true
+    }
+    let mutated = false
+    if (
+      queryThunk.fulfilled.match(action) ||
+      queryThunk.rejected.match(action)
+    ) {
+      const state = mutableState[action.meta.arg.queryCacheKey] || {}
+      const key = `${action.meta.requestId}_running`
+      mutated ||= !!state[key]
+      delete state[key]
     }
     if (queryThunk.rejected.match(action)) {
       const {
@@ -87,14 +73,37 @@ export const buildBatchedActionsHandler: InternalHandlerBuilder<
         substate[requestId] =
           arg.subscriptionOptions ?? substate[requestId] ?? {}
 
-        return true
+        mutated = true
       }
     }
 
-    return false
+    return mutated
+  }
+
+  const getSubscriptions = () => internalState.currentSubscriptions
+  const getSubscriptionCount = (queryCacheKey: string) => {
+    const subscriptions = getSubscriptions()
+    const subscriptionsForQueryArg = subscriptions[queryCacheKey] ?? {}
+    return countObjectKeys(subscriptionsForQueryArg)
+  }
+  const isRequestSubscribed = (queryCacheKey: string, requestId: string) => {
+    const subscriptions = getSubscriptions()
+    return !!subscriptions?.[queryCacheKey]?.[requestId]
+  }
+
+  const subscriptionSelectors: SubscriptionSelectors = {
+    getSubscriptions,
+    getSubscriptionCount,
+    isRequestSubscribed,
   }
 
-  return (action, mwApi) => {
+  return (
+    action,
+    mwApi
+  ): [
+    actionShouldContinue: boolean,
+    result: SubscriptionSelectors | boolean
+  ] => {
     if (!previousSubscriptions) {
       // Initialize it the first time this handler runs
       previousSubscriptions = JSON.parse(
@@ -104,16 +113,16 @@ export const buildBatchedActionsHandler: InternalHandlerBuilder<
 
     if (api.util.resetApiState.match(action)) {
       previousSubscriptions = internalState.currentSubscriptions = {}
+      updateSyncTimer = null
       return [true, false]
     }
 
     // Intercept requests by hooks to see if they're subscribed
-    // Necessary because we delay updating store state to the end of the tick
-    if (api.internalActions.internal_probeSubscription.match(action)) {
-      const { queryCacheKey, requestId } = action.payload
-      const hasSubscription =
-        !!internalState.currentSubscriptions[queryCacheKey]?.[requestId]
-      return [false, hasSubscription]
+    // We return the internal state reference so that hooks
+    // can do their own checks to see if they're still active.
+    // It's stupid and hacky, but it does cut down on some dispatch calls.
+    if (api.internalActions.internal_getRTKQSubscriptions.match(action)) {
+      return [false, subscriptionSelectors]
     }
 
     // Update subscription data based on this action
@@ -122,9 +131,16 @@ export const buildBatchedActionsHandler: InternalHandlerBuilder<
       action
     )
 
+    let actionShouldContinue = true
+
     if (didMutate) {
-      if (!dispatchQueued) {
-        queueMicrotaskShim(() => {
+      if (!updateSyncTimer) {
+        // We only use the subscription state for the Redux DevTools at this point,
+        // as the real data is kept here in the middleware.
+        // Given that, we can throttle synchronizing this state significantly to
+        // save on overall perf.
+        // In 1.9, it was updated in a microtask, but now we do it at most every 500ms.
+        updateSyncTimer = setTimeout(() => {
           // Deep clone the current subscription data
           const newSubscriptions: SubscriptionState = JSON.parse(
             JSON.stringify(internalState.currentSubscriptions)
@@ -139,24 +155,23 @@ export const buildBatchedActionsHandler: InternalHandlerBuilder<
           mwApi.next(api.internalActions.subscriptionsUpdated(patches))
           // Save the cloned state for later reference
           previousSubscriptions = newSubscriptions
-          dispatchQueued = false
-        })
-        dispatchQueued = true
+          updateSyncTimer = null
+        }, 500)
       }
 
       const isSubscriptionSliceAction =
-        !!action.type?.startsWith(subscriptionsPrefix)
+        typeof action.type == 'string' &&
+        !!action.type.startsWith(subscriptionsPrefix)
+
       const isAdditionalSubscriptionAction =
         queryThunk.rejected.match(action) &&
         action.meta.condition &&
         !!action.meta.arg.subscribe
 
-      const actionShouldContinue =
+      actionShouldContinue =
         !isSubscriptionSliceAction && !isAdditionalSubscriptionAction
-
-      return [actionShouldContinue, false]
     }
 
-    return [true, false]
+    return [actionShouldContinue, false]
   }
 }
diff --git a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts
--- a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts
+++ b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts
@@ -1,5 +1,5 @@
-import { isAsyncThunkAction, isFulfilled } from '@reduxjs/toolkit'
-import type { AnyAction } from 'redux'
+import { isAsyncThunkAction, isFulfilled } from '../rtkImports'
+import type { UnknownAction } from 'redux'
 import type { ThunkDispatch } from 'redux-thunk'
 import type { BaseQueryFn, BaseQueryMeta } from '../../baseQueryTypes'
 import { DefinitionType } from '../../endpointDefinitions'
@@ -65,7 +65,7 @@ declare module '../../endpointDefinitions' {
     /**
      * The dispatch method for the store
      */
-    dispatch: ThunkDispatch<any, any, AnyAction>
+    dispatch: ThunkDispatch<any, any, UnknownAction>
     /**
      * A method to get the current state
      */
diff --git a/packages/toolkit/src/query/core/buildMiddleware/index.ts b/packages/toolkit/src/query/core/buildMiddleware/index.ts
--- a/packages/toolkit/src/query/core/buildMiddleware/index.ts
+++ b/packages/toolkit/src/query/core/buildMiddleware/index.ts
@@ -1,5 +1,10 @@
-import type { AnyAction, Middleware, ThunkDispatch } from '@reduxjs/toolkit'
-import { createAction } from '@reduxjs/toolkit'
+import type {
+  Action,
+  Middleware,
+  ThunkDispatch,
+  UnknownAction,
+} from '@reduxjs/toolkit'
+import { isAction, createAction } from '../rtkImports'
 
 import type {
   EndpointDefinitions,
@@ -35,13 +40,8 @@ export function buildMiddleware<
     >(`${reducerPath}/invalidateTags`),
   }
 
-  const isThisApiSliceAction = (action: AnyAction) => {
-    return (
-      !!action &&
-      typeof action.type === 'string' &&
-      action.type.startsWith(`${reducerPath}/`)
-    )
-  }
+  const isThisApiSliceAction = (action: Action) =>
+    action.type.startsWith(`${reducerPath}/`)
 
   const handlerBuilders: InternalHandlerBuilder[] = [
     buildDevCheckHandler,
@@ -55,7 +55,7 @@ export function buildMiddleware<
   const middleware: Middleware<
     {},
     RootState<Definitions, string, ReducerPath>,
-    ThunkDispatch<any, any, AnyAction>
+    ThunkDispatch<any, any, UnknownAction>
   > = (mwApi) => {
     let initialized = false
 
@@ -71,6 +71,7 @@ export function buildMiddleware<
       >),
       internalState,
       refetchQuery,
+      isThisApiSliceAction,
     }
 
     const handlers = handlerBuilders.map((build) => build(builderArgs))
@@ -80,6 +81,9 @@ export function buildMiddleware<
 
     return (next) => {
       return (action) => {
+        if (!isAction(action)) {
+          return next(action)
+        }
         if (!initialized) {
           initialized = true
           // dispatch before any other action
@@ -90,18 +94,15 @@ export function buildMiddleware<
 
         const stateBefore = mwApi.getState()
 
-        const [actionShouldContinue, hasSubscription] = batchedActionsHandler(
-          action,
-          mwApiWithNext,
-          stateBefore
-        )
+        const [actionShouldContinue, internalProbeResult] =
+          batchedActionsHandler(action, mwApiWithNext, stateBefore)
 
         let res: any
 
         if (actionShouldContinue) {
           res = next(action)
         } else {
-          res = hasSubscription
+          res = internalProbeResult
         }
 
         if (!!mwApi.getState()[reducerPath]) {
diff --git a/packages/toolkit/src/query/core/buildMiddleware/invalidationByTags.ts b/packages/toolkit/src/query/core/buildMiddleware/invalidationByTags.ts
--- a/packages/toolkit/src/query/core/buildMiddleware/invalidationByTags.ts
+++ b/packages/toolkit/src/query/core/buildMiddleware/invalidationByTags.ts
@@ -1,24 +1,36 @@
-import { isAnyOf, isFulfilled, isRejectedWithValue } from '@reduxjs/toolkit'
+import {
+  isAnyOf,
+  isFulfilled,
+  isRejected,
+  isRejectedWithValue,
+} from '../rtkImports'
 
-import type { FullTagDescription } from '../../endpointDefinitions'
+import type {
+  EndpointDefinitions,
+  FullTagDescription,
+} from '../../endpointDefinitions'
 import { calculateProvidedBy } from '../../endpointDefinitions'
-import type { QueryCacheKey } from '../apiState'
+import type { CombinedState, QueryCacheKey } from '../apiState'
 import { QueryStatus } from '../apiState'
 import { calculateProvidedByThunk } from '../buildThunks'
 import type {
   SubMiddlewareApi,
   InternalHandlerBuilder,
   ApiMiddlewareInternalHandler,
+  InternalMiddlewareState,
 } from './types'
+import { countObjectKeys } from '../../utils/countObjectKeys'
 
 export const buildInvalidationByTagsHandler: InternalHandlerBuilder = ({
   reducerPath,
   context,
   context: { endpointDefinitions },
   mutationThunk,
+  queryThunk,
   api,
   assertTagType,
   refetchQuery,
+  internalState,
 }) => {
   const { removeQueryResult } = api.internalActions
   const isThunkActionWithTags = isAnyOf(
@@ -26,6 +38,13 @@ export const buildInvalidationByTagsHandler: InternalHandlerBuilder = ({
     isRejectedWithValue(mutationThunk)
   )
 
+  const isQueryEnd = isAnyOf(
+    isFulfilled(mutationThunk, queryThunk),
+    isRejected(mutationThunk, queryThunk)
+  )
+
+  let pendingTagInvalidations: FullTagDescription<string>[] = []
+
   const handler: ApiMiddlewareInternalHandler = (action, mwApi) => {
     if (isThunkActionWithTags(action)) {
       invalidateTags(
@@ -37,9 +56,9 @@ export const buildInvalidationByTagsHandler: InternalHandlerBuilder = ({
         ),
         mwApi
       )
-    }
-
-    if (api.util.invalidateTags.match(action)) {
+    } else if (isQueryEnd(action)) {
+      invalidateTags([], mwApi)
+    } else if (api.util.invalidateTags.match(action)) {
       invalidateTags(
         calculateProvidedBy(
           action.payload,
@@ -54,23 +73,44 @@ export const buildInvalidationByTagsHandler: InternalHandlerBuilder = ({
     }
   }
 
+  function hasPendingRequests(state: CombinedState<EndpointDefinitions, string, string>) {
+    for (const key in state.queries) {
+      if (state.queries[key]?.status === QueryStatus.pending) return true;
+    }
+    for (const key in state.mutations) {
+      if (state.mutations[key]?.status === QueryStatus.pending) return true;
+    }
+    return false;
+  }
+
   function invalidateTags(
-    tags: readonly FullTagDescription<string>[],
+    newTags: readonly FullTagDescription<string>[],
     mwApi: SubMiddlewareApi
   ) {
     const rootState = mwApi.getState()
     const state = rootState[reducerPath]
 
+    pendingTagInvalidations.push(...newTags)
+
+    if (state.config.invalidationBehavior === 'delayed' && hasPendingRequests(state)) {
+      return
+    }
+
+    const tags = pendingTagInvalidations
+    pendingTagInvalidations = []
+    if (tags.length === 0) return
+
     const toInvalidate = api.util.selectInvalidatedBy(rootState, tags)
 
     context.batch(() => {
       const valuesArray = Array.from(toInvalidate.values())
       for (const { queryCacheKey } of valuesArray) {
         const querySubState = state.queries[queryCacheKey]
-        const subscriptionSubState = state.subscriptions[queryCacheKey] ?? {}
+        const subscriptionSubState =
+          internalState.currentSubscriptions[queryCacheKey] ?? {}
 
         if (querySubState) {
-          if (Object.keys(subscriptionSubState).length === 0) {
+          if (countObjectKeys(subscriptionSubState) === 0) {
             mwApi.dispatch(
               removeQueryResult({
                 queryCacheKey: queryCacheKey as QueryCacheKey,
diff --git a/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts b/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts
--- a/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts
+++ b/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts
@@ -1,4 +1,4 @@
-import { isPending, isRejected, isFulfilled } from '@reduxjs/toolkit'
+import { isPending, isRejected, isFulfilled } from '../rtkImports'
 import type {
   BaseQueryError,
   BaseQueryFn,
diff --git a/packages/toolkit/src/query/core/buildMiddleware/types.ts b/packages/toolkit/src/query/core/buildMiddleware/types.ts
--- a/packages/toolkit/src/query/core/buildMiddleware/types.ts
+++ b/packages/toolkit/src/query/core/buildMiddleware/types.ts
@@ -1,10 +1,10 @@
 import type {
-  AnyAction,
+  Action,
   AsyncThunkAction,
-  Dispatch,
   Middleware,
   MiddlewareAPI,
   ThunkDispatch,
+  UnknownAction,
 } from '@reduxjs/toolkit'
 
 import type { Api, ApiContext } from '../../apiTypes'
@@ -32,6 +32,12 @@ export interface InternalMiddlewareState {
   currentSubscriptions: SubscriptionState
 }
 
+export interface SubscriptionSelectors {
+  getSubscriptions: () => SubscriptionState
+  getSubscriptionCount: (queryCacheKey: string) => number
+  isRequestSubscribed: (queryCacheKey: string, requestId: string) => boolean
+}
+
 export interface BuildMiddlewareInput<
   Definitions extends EndpointDefinitions,
   ReducerPath extends string,
@@ -46,7 +52,7 @@ export interface BuildMiddlewareInput<
 }
 
 export type SubMiddlewareApi = MiddlewareAPI<
-  ThunkDispatch<any, any, AnyAction>,
+  ThunkDispatch<any, any, UnknownAction>,
   RootState<EndpointDefinitions, string, string>
 >
 
@@ -61,6 +67,7 @@ export interface BuildSubMiddlewareInput
     queryCacheKey: string,
     override?: Partial<QueryThunkArg>
   ): AsyncThunkAction<ThunkResult, QueryThunkArg, {}>
+  isThisApiSliceAction: (action: Action) => boolean
 }
 
 export type SubMiddlewareBuilder = (
@@ -68,14 +75,16 @@ export type SubMiddlewareBuilder = (
 ) => Middleware<
   {},
   RootState<EndpointDefinitions, string, string>,
-  ThunkDispatch<any, any, AnyAction>
+  ThunkDispatch<any, any, UnknownAction>
 >
 
-export type ApiMiddlewareInternalHandler<ReturnType = void> = (
-  action: AnyAction,
-  mwApi: SubMiddlewareApi & { next: Dispatch<AnyAction> },
+type MwNext = Parameters<ReturnType<Middleware>>[0]
+
+export type ApiMiddlewareInternalHandler<Return = void> = (
+  action: Action,
+  mwApi: SubMiddlewareApi & { next: MwNext },
   prevState: RootState<EndpointDefinitions, string, string>
-) => ReturnType
+) => Return
 
 export type InternalHandlerBuilder<ReturnType = void> = (
   input: BuildSubMiddlewareInput
diff --git a/packages/toolkit/src/query/core/buildMiddleware/windowEventHandling.ts b/packages/toolkit/src/query/core/buildMiddleware/windowEventHandling.ts
--- a/packages/toolkit/src/query/core/buildMiddleware/windowEventHandling.ts
+++ b/packages/toolkit/src/query/core/buildMiddleware/windowEventHandling.ts
@@ -6,6 +6,7 @@ import type {
   InternalHandlerBuilder,
   SubMiddlewareApi,
 } from './types'
+import { countObjectKeys } from '../../utils/countObjectKeys'
 
 export const buildWindowEventHandler: InternalHandlerBuilder = ({
   reducerPath,
@@ -50,7 +51,7 @@ export const buildWindowEventHandler: InternalHandlerBuilder = ({
             state.config[type])
 
         if (shouldRefetch) {
-          if (Object.keys(subscriptionSubState).length === 0) {
+          if (countObjectKeys(subscriptionSubState) === 0) {
             api.dispatch(
               removeQueryResult({
                 queryCacheKey: queryCacheKey as QueryCacheKey,
diff --git a/packages/toolkit/src/query/core/buildSelectors.ts b/packages/toolkit/src/query/core/buildSelectors.ts
--- a/packages/toolkit/src/query/core/buildSelectors.ts
+++ b/packages/toolkit/src/query/core/buildSelectors.ts
@@ -1,10 +1,12 @@
-import { createNextState, createSelector } from '@reduxjs/toolkit'
+import { createNextState, createSelector } from './rtkImports'
 import type {
   MutationSubState,
   QuerySubState,
   RootState as _RootState,
   RequestStatusFlags,
   QueryCacheKey,
+  QueryKeys,
+  QueryState,
 } from './apiState'
 import { QueryStatus, getRequestStatusFlags } from './apiState'
 import type {
@@ -45,8 +47,6 @@ export type SkipToken = typeof skipToken
  * return an uninitialized state.
  */
 export const skipToken = /* @__PURE__ */ Symbol.for('RTKQ/skipToken')
-/** @deprecated renamed to `skipToken` */
-export const skipSelector = skipToken
 
 declare module './module' {
   export interface ApiEndpointQuery<
@@ -132,7 +132,12 @@ export function buildSelectors<
   const selectSkippedQuery = (state: RootState) => defaultQuerySubState
   const selectSkippedMutation = (state: RootState) => defaultMutationSubState
 
-  return { buildQuerySelector, buildMutationSelector, selectInvalidatedBy }
+  return {
+    buildQuerySelector,
+    buildMutationSelector,
+    selectInvalidatedBy,
+    selectCachedArgsForQuery,
+  }
 
   function withRequestFlags<T extends { status: QueryStatus }>(
     substate: T
@@ -240,4 +245,22 @@ export function buildSelectors<
       })
     )
   }
+
+  function selectCachedArgsForQuery<QueryName extends QueryKeys<Definitions>>(
+    state: RootState,
+    queryName: QueryName
+  ): Array<QueryArgFrom<Definitions[QueryName]>> {
+    return Object.values(state[reducerPath].queries as QueryState<any>)
+      .filter(
+        (
+          entry
+        ): entry is Exclude<
+          QuerySubState<Definitions[QueryName]>,
+          { status: QueryStatus.uninitialized }
+        > =>
+          entry?.endpointName === queryName &&
+          entry.status !== QueryStatus.uninitialized
+      )
+      .map((entry) => entry.originalArgs)
+  }
 }
diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts
--- a/packages/toolkit/src/query/core/buildSlice.ts
+++ b/packages/toolkit/src/query/core/buildSlice.ts
@@ -1,4 +1,4 @@
-import type { AnyAction, PayloadAction } from '@reduxjs/toolkit'
+import type { Action, PayloadAction, UnknownAction } from '@reduxjs/toolkit'
 import {
   combineReducers,
   createAction,
@@ -8,9 +8,8 @@ import {
   isRejectedWithValue,
   createNextState,
   prepareAutoBatched,
-} from '@reduxjs/toolkit'
+} from './rtkImports'
 import type {
-  CombinedState as CombinedQueryState,
   QuerySubstateIdentifier,
   QuerySubState,
   MutationSubstateIdentifier,
@@ -148,12 +147,9 @@ export function buildSlice({
       builder
         .addCase(queryThunk.pending, (draft, { meta, meta: { arg } }) => {
           const upserting = isUpsertQuery(arg)
-          if (arg.subscribe || upserting) {
-            // only initialize substate if we want to subscribe to it
-            draft[arg.queryCacheKey] ??= {
-              status: QueryStatus.uninitialized,
-              endpointName: arg.endpointName,
-            }
+          draft[arg.queryCacheKey] ??= {
+            status: QueryStatus.uninitialized,
+            endpointName: arg.endpointName,
           }
 
           updateQuerySubstateIfExists(draft, arg.queryCacheKey, (substate) => {
@@ -447,12 +443,7 @@ export function buildSlice({
       ) {
         // Dummy
       },
-      internal_probeSubscription(
-        d,
-        a: PayloadAction<{ queryCacheKey: string; requestId: string }>
-      ) {
-        // dummy
-      },
+      internal_getRTKQSubscriptions() {},
     },
   })
 
@@ -505,9 +496,7 @@ export function buildSlice({
     },
   })
 
-  const combinedReducer = combineReducers<
-    CombinedQueryState<any, string, string>
-  >({
+  const combinedReducer = combineReducers({
     queries: querySlice.reducer,
     mutations: mutationSlice.reducer,
     provided: invalidationSlice.reducer,
@@ -525,8 +514,6 @@ export function buildSlice({
     ...internalSubscriptionsSlice.actions,
     ...mutationSlice.actions,
     ...invalidationSlice.actions,
-    /** @deprecated has been renamed to `removeMutationResult` */
-    unsubscribeMutationResult: mutationSlice.actions.removeMutationResult,
     resetApiState,
   }
 
diff --git a/packages/toolkit/src/query/core/buildThunks.ts b/packages/toolkit/src/query/core/buildThunks.ts
--- a/packages/toolkit/src/query/core/buildThunks.ts
+++ b/packages/toolkit/src/query/core/buildThunks.ts
@@ -24,23 +24,23 @@ import type {
 } from '../endpointDefinitions'
 import { isQueryDefinition } from '../endpointDefinitions'
 import { calculateProvidedBy } from '../endpointDefinitions'
-import type { AsyncThunkPayloadCreator, Draft } from '@reduxjs/toolkit'
+import type {
+  AsyncThunkPayloadCreator,
+  Draft,
+  UnknownAction,
+} from '@reduxjs/toolkit'
 import {
   isAllOf,
   isFulfilled,
   isPending,
   isRejected,
   isRejectedWithValue,
-} from '@reduxjs/toolkit'
+  createAsyncThunk,
+  SHOULD_AUTOBATCH,
+} from './rtkImports'
 import type { Patch } from 'immer'
 import { isDraftable, produceWithPatches } from 'immer'
-import type {
-  AnyAction,
-  ThunkAction,
-  ThunkDispatch,
-  AsyncThunk,
-} from '@reduxjs/toolkit'
-import { createAsyncThunk, SHOULD_AUTOBATCH } from '@reduxjs/toolkit'
+import type { ThunkAction, ThunkDispatch, AsyncThunk } from '@reduxjs/toolkit'
 
 import { HandledError } from '../HandledError'
 
@@ -167,7 +167,7 @@ export type PatchQueryDataThunk<
   args: QueryArgFrom<Definitions[EndpointName]>,
   patches: readonly Patch[],
   updateProvided?: boolean
-) => ThunkAction<void, PartialState, any, AnyAction>
+) => ThunkAction<void, PartialState, any, UnknownAction>
 
 export type UpdateQueryDataThunk<
   Definitions extends EndpointDefinitions,
@@ -177,7 +177,7 @@ export type UpdateQueryDataThunk<
   args: QueryArgFrom<Definitions[EndpointName]>,
   updateRecipe: Recipe<ResultTypeFrom<Definitions[EndpointName]>>,
   updateProvided?: boolean
-) => ThunkAction<PatchCollection, PartialState, any, AnyAction>
+) => ThunkAction<PatchCollection, PartialState, any, UnknownAction>
 
 export type UpsertQueryDataThunk<
   Definitions extends EndpointDefinitions,
@@ -194,7 +194,7 @@ export type UpsertQueryDataThunk<
   >,
   PartialState,
   any,
-  AnyAction
+  UnknownAction
 >
 
 /**
@@ -594,7 +594,7 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".`
       endpointName: EndpointName,
       arg: any,
       options: PrefetchOptions
-    ): ThunkAction<void, any, any, AnyAction> =>
+    ): ThunkAction<void, any, any, UnknownAction> =>
     (dispatch: ThunkDispatch<any, any, any>, getState: () => any) => {
       const force = hasTheForce(options) && options.force
       const maxAge = hasMaxAge(options) && options.ifOlderThan
@@ -629,7 +629,7 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".`
     }
 
   function matchesEndpoint(endpointName: string) {
-    return (action: any): action is AnyAction =>
+    return (action: any): action is UnknownAction =>
       action?.meta?.arg?.endpointName === endpointName
   }
 
diff --git a/packages/toolkit/src/query/core/module.ts b/packages/toolkit/src/query/core/module.ts
--- a/packages/toolkit/src/query/core/module.ts
+++ b/packages/toolkit/src/query/core/module.ts
@@ -9,11 +9,11 @@ import type {
 import { buildThunks } from './buildThunks'
 import type {
   ActionCreatorWithPayload,
-  AnyAction,
   Middleware,
   Reducer,
   ThunkAction,
   ThunkDispatch,
+  UnknownAction,
 } from '@reduxjs/toolkit'
 import type {
   EndpointDefinitions,
@@ -71,7 +71,8 @@ export type CoreModule =
   | ReferenceQueryLifecycle
   | ReferenceCacheCollection
 
-export interface ThunkWithReturnValue<T> extends ThunkAction<T, any, any, AnyAction> {}
+export interface ThunkWithReturnValue<T>
+  extends ThunkAction<T, any, any, UnknownAction> {}
 
 declare module '../apiTypes' {
   export interface ApiModules<
@@ -115,7 +116,7 @@ declare module '../apiTypes' {
        */
       reducer: Reducer<
         CombinedState<Definitions, TagTypes, ReducerPath>,
-        AnyAction
+        UnknownAction
       >
       /**
        * This is a standard redux middleware and is responsible for things like polling, garbage collection and a handful of other things. Make sure it's included in your store.
@@ -133,34 +134,12 @@ declare module '../apiTypes' {
       middleware: Middleware<
         {},
         RootState<Definitions, string, ReducerPath>,
-        ThunkDispatch<any, any, AnyAction>
+        ThunkDispatch<any, any, UnknownAction>
       >
       /**
        * A collection of utility thunks for various situations.
        */
       util: {
-        /**
-         * This method had to be removed due to a conceptual bug in RTK.
-         *
-         * Despite TypeScript errors, it will continue working in the "buggy" way it did
-         * before in production builds and will be removed in the next major release.
-         *
-         * Nonetheless, you should immediately replace it with the new recommended approach.
-         * See https://redux-toolkit.js.org/rtk-query/usage/server-side-rendering for new guidance on SSR.
-         *
-         * Please see https://github.com/reduxjs/redux-toolkit/pull/2481 for details.
-         * @deprecated
-         */
-        getRunningOperationPromises: never // this is now types as `never` to immediately throw TS errors on use, but still allow for a comment
-
-        /**
-         * This method had to be removed due to a conceptual bug in RTK.
-         * It has been replaced by `api.util.getRunningQueryThunk` and `api.util.getRunningMutationThunk`.
-         * Please see https://github.com/reduxjs/redux-toolkit/pull/2481 for details.
-         * @deprecated
-         */
-        getRunningOperationPromise: never // this is now types as `never` to immediately throw TS errors on use, but still allow for a comment
-
         /**
          * A thunk that (if dispatched) will return a specific running query, identified
          * by `endpointName` and `args`.
@@ -242,7 +221,7 @@ declare module '../apiTypes' {
           endpointName: EndpointName,
           arg: QueryArgFrom<Definitions[EndpointName]>,
           options: PrefetchOptions
-        ): ThunkAction<void, any, any, AnyAction>
+        ): ThunkAction<void, any, any, UnknownAction>
         /**
          * A Redux thunk action creator that, when dispatched, creates and applies a set of JSON diff/patch objects to the current state. This immediately updates the Redux state with those changes.
          *
@@ -268,11 +247,7 @@ declare module '../apiTypes' {
           Definitions,
           RootState<Definitions, string, ReducerPath>
         >
-        /** @deprecated renamed to `updateQueryData` */
-        updateQueryResult: UpdateQueryDataThunk<
-          Definitions,
-          RootState<Definitions, string, ReducerPath>
-        >
+
         /**
          * A Redux thunk action creator that, when dispatched, acts as an artificial API request to upsert a value into the cache.
          *
@@ -326,11 +301,7 @@ declare module '../apiTypes' {
           Definitions,
           RootState<Definitions, string, ReducerPath>
         >
-        /** @deprecated renamed to `patchQueryData` */
-        patchQueryResult: PatchQueryDataThunk<
-          Definitions,
-          RootState<Definitions, string, ReducerPath>
-        >
+
         /**
          * A Redux action creator that can be dispatched to manually reset the api state completely. This will immediately remove all existing cache entries, and all queries will be considered 'uninitialized'.
          *
@@ -385,6 +356,16 @@ declare module '../apiTypes' {
           originalArgs: any
           queryCacheKey: string
         }>
+
+        /**
+         * A function to select all arguments currently cached for a given endpoint.
+         *
+         * Can be used for mutations that want to do optimistic updates instead of invalidating a set of tags, but don't know exactly what they need to update.
+         */
+        selectCachedArgsForQuery: <QueryName extends QueryKeys<Definitions>>(
+          state: RootState<Definitions, string, ReducerPath>,
+          queryName: QueryName
+        ) => Array<QueryArgFrom<Definitions[QueryName]>>
       }
       /**
        * Endpoints based on the input endpoints provided to `createApi`, containing `select` and `action matchers`.
@@ -471,6 +452,7 @@ export const coreModule = (): Module<CoreModule> => ({
       refetchOnMountOrArgChange,
       refetchOnFocus,
       refetchOnReconnect,
+      invalidationBehavior,
     },
     context
   ) {
@@ -533,6 +515,7 @@ export const coreModule = (): Module<CoreModule> => ({
         refetchOnMountOrArgChange,
         keepUnusedDataFor,
         reducerPath,
+        invalidationBehavior,
       },
     })
 
@@ -557,13 +540,17 @@ export const coreModule = (): Module<CoreModule> => ({
 
     safeAssign(api, { reducer: reducer as any, middleware })
 
-    const { buildQuerySelector, buildMutationSelector, selectInvalidatedBy } =
-      buildSelectors({
-        serializeQueryArgs: serializeQueryArgs as any,
-        reducerPath,
-      })
+    const {
+      buildQuerySelector,
+      buildMutationSelector,
+      selectInvalidatedBy,
+      selectCachedArgsForQuery,
+    } = buildSelectors({
+      serializeQueryArgs: serializeQueryArgs as any,
+      reducerPath,
+    })
 
-    safeAssign(api.util, { selectInvalidatedBy })
+    safeAssign(api.util, { selectInvalidatedBy, selectCachedArgsForQuery })
 
     const {
       buildInitiateQuery,
@@ -572,8 +559,6 @@ export const coreModule = (): Module<CoreModule> => ({
       getRunningMutationsThunk,
       getRunningQueriesThunk,
       getRunningQueryThunk,
-      getRunningOperationPromises,
-      removalWarning,
     } = buildInitiate({
       queryThunk,
       mutationThunk,
@@ -583,8 +568,6 @@ export const coreModule = (): Module<CoreModule> => ({
     })
 
     safeAssign(api.util, {
-      getRunningOperationPromises: getRunningOperationPromises as any,
-      getRunningOperationPromise: removalWarning as any,
       getRunningMutationThunk,
       getRunningMutationsThunk,
       getRunningQueryThunk,
diff --git a/packages/toolkit/src/query/core/rtkImports.ts b/packages/toolkit/src/query/core/rtkImports.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/query/core/rtkImports.ts
@@ -0,0 +1,24 @@
+// This file exists to consolidate all of the imports from the `@reduxjs/toolkit` package.
+// ESBuild does not de-duplicate imports, so this file is used to ensure that each method
+// imported is only listed once, and there's only one mention of the `@reduxjs/toolkit` package.
+
+export {
+  createAction,
+  createSlice,
+  createSelector,
+  createAsyncThunk,
+  combineReducers,
+  createNextState,
+  isAnyOf,
+  isAllOf,
+  isAction,
+  isPending,
+  isRejected,
+  isFulfilled,
+  isRejectedWithValue,
+  isAsyncThunkAction,
+  prepareAutoBatched,
+  SHOULD_AUTOBATCH,
+  isPlainObject,
+  nanoid,
+} from '@reduxjs/toolkit'
diff --git a/packages/toolkit/src/query/core/setupListeners.ts b/packages/toolkit/src/query/core/setupListeners.ts
--- a/packages/toolkit/src/query/core/setupListeners.ts
+++ b/packages/toolkit/src/query/core/setupListeners.ts
@@ -2,7 +2,7 @@ import type {
   ThunkDispatch,
   ActionCreatorWithoutPayload, // Workaround for API-Extractor
 } from '@reduxjs/toolkit'
-import { createAction } from '@reduxjs/toolkit'
+import { createAction } from './rtkImports'
 
 export const onFocus = /* @__PURE__ */ createAction('__rtkq/focused')
 export const onFocusLost = /* @__PURE__ */ createAction('__rtkq/unfocused')
diff --git a/packages/toolkit/src/query/createApi.ts b/packages/toolkit/src/query/createApi.ts
--- a/packages/toolkit/src/query/createApi.ts
+++ b/packages/toolkit/src/query/createApi.ts
@@ -8,10 +8,10 @@ import type {
   EndpointDefinitions,
 } from './endpointDefinitions'
 import { DefinitionType, isQueryDefinition } from './endpointDefinitions'
-import { nanoid } from '@reduxjs/toolkit'
-import type { AnyAction } from '@reduxjs/toolkit'
+import { nanoid } from './core/rtkImports'
+import type { UnknownAction } from '@reduxjs/toolkit'
 import type { NoInfer } from './tsHelpers'
-import { defaultMemoize } from 'reselect'
+import { weakMapMemoize } from 'reselect'
 
 export interface CreateApiOptions<
   BaseQuery extends BaseQueryFn,
@@ -151,6 +151,16 @@ export interface CreateApiOptions<
    * Note: requires [`setupListeners`](./setupListeners) to have been called.
    */
   refetchOnReconnect?: boolean
+  /**
+   * Defaults to `'immediately'`. This setting allows you to control when tags are invalidated after a mutation.
+   *
+   * - `'immediately'`: Queries are invalidated instantly after the mutation finished, even if they are running.
+   *   If the query provides tags that were invalidated while it ran, it won't be re-fetched.
+   * - `'delayed'`: Invalidation only happens after all queries and mutations are settled.
+   *   This ensures that queries are always invalidated correctly and automatically "batches" invalidations of concurrent mutations.
+   *   Note that if you constantly have some queries (or mutations) running, this can delay tag invalidations indefinitely.
+   */
+  invalidationBehavior?: 'delayed' | 'immediately'
   /**
    * A function that is passed every dispatched action. If this returns something other than `undefined`,
    * that return value will be used to rehydrate fulfilled & errored queries.
@@ -159,14 +169,21 @@ export interface CreateApiOptions<
    *
    * ```ts
    * // codeblock-meta title="next-redux-wrapper rehydration example"
+   * import type { Action, PayloadAction } from '@reduxjs/toolkit'
    * import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
    * import { HYDRATE } from 'next-redux-wrapper'
    *
+   * type RootState = any; // normally inferred from state
+   *
+   * function isHydrateAction(action: Action): action is PayloadAction<RootState> {
+   *   return action.type === HYDRATE
+   * }
+   *
    * export const api = createApi({
    *   baseQuery: fetchBaseQuery({ baseUrl: '/' }),
    *   // highlight-start
    *   extractRehydrationInfo(action, { reducerPath }) {
-   *     if (action.type === HYDRATE) {
+   *     if (isHydrateAction(action)) {
    *       return action.payload[reducerPath]
    *     }
    *   },
@@ -178,7 +195,7 @@ export interface CreateApiOptions<
    * ```
    */
   extractRehydrationInfo?: (
-    action: AnyAction,
+    action: UnknownAction,
     {
       reducerPath,
     }: {
@@ -219,7 +236,13 @@ export type CreateApi<Modules extends ModuleName> = {
  * const MyContext = React.createContext<ReactReduxContextValue>(null as any);
  * const customCreateApi = buildCreateApi(
  *   coreModule(),
- *   reactHooksModule({ useDispatch: createDispatchHook(MyContext) })
+ *   reactHooksModule({
+ *     hooks: {
+ *       useDispatch: createDispatchHook(MyContext),
+ *       useSelector: createSelectorHook(MyContext),
+ *       useStore: createStoreHook(MyContext)
+ *     }
+ *   })
  * );
  * ```
  *
@@ -230,7 +253,7 @@ export function buildCreateApi<Modules extends [Module<any>, ...Module<any>[]]>(
   ...modules: Modules
 ): CreateApi<Modules[number]['name']> {
   return function baseCreateApi(options) {
-    const extractRehydrationInfo = defaultMemoize((action: AnyAction) =>
+    const extractRehydrationInfo = weakMapMemoize((action: UnknownAction) =>
       options.extractRehydrationInfo?.(action, {
         reducerPath: (options.reducerPath ?? 'api') as any,
       })
@@ -242,6 +265,7 @@ export function buildCreateApi<Modules extends [Module<any>, ...Module<any>[]]>(
       refetchOnMountOrArgChange: false,
       refetchOnFocus: false,
       refetchOnReconnect: false,
+      invalidationBehavior: 'delayed',
       ...options,
       extractRehydrationInfo,
       serializeQueryArgs(queryArgsApi) {
@@ -280,7 +304,7 @@ export function buildCreateApi<Modules extends [Module<any>, ...Module<any>[]]>(
       },
       apiUid: nanoid(),
       extractRehydrationInfo,
-      hasRehydrationInfo: defaultMemoize(
+      hasRehydrationInfo: weakMapMemoize(
         (action) => extractRehydrationInfo(action) != null
       ),
     }
diff --git a/packages/toolkit/src/query/defaultSerializeQueryArgs.ts b/packages/toolkit/src/query/defaultSerializeQueryArgs.ts
--- a/packages/toolkit/src/query/defaultSerializeQueryArgs.ts
+++ b/packages/toolkit/src/query/defaultSerializeQueryArgs.ts
@@ -1,6 +1,6 @@
 import type { QueryCacheKey } from './core/apiState'
 import type { EndpointDefinition } from './endpointDefinitions'
-import { isPlainObject } from '@reduxjs/toolkit'
+import { isPlainObject } from './core/rtkImports'
 
 const cache: WeakMap<any, string> | undefined = WeakMap
   ? new WeakMap()
diff --git a/packages/toolkit/src/query/endpointDefinitions.ts b/packages/toolkit/src/query/endpointDefinitions.ts
--- a/packages/toolkit/src/query/endpointDefinitions.ts
+++ b/packages/toolkit/src/query/endpointDefinitions.ts
@@ -1,4 +1,3 @@
-import type { AnyAction, ThunkDispatch } from '@reduxjs/toolkit'
 import type { SerializeQueryArgs } from './defaultSerializeQueryArgs'
 import type { QuerySubState, RootState } from './core/apiState'
 import type {
@@ -231,20 +230,6 @@ export type ResultDescription<
   | ReadonlyArray<TagDescription<TagTypes>>
   | GetResultDescriptionFn<TagTypes, ResultType, QueryArg, ErrorType, MetaType>
 
-/** @deprecated please use `onQueryStarted` instead */
-export interface QueryApi<ReducerPath extends string, Context extends {}> {
-  /** @deprecated please use `onQueryStarted` instead */
-  dispatch: ThunkDispatch<any, any, AnyAction>
-  /** @deprecated please use `onQueryStarted` instead */
-  getState(): RootState<any, any, ReducerPath>
-  /** @deprecated please use `onQueryStarted` instead */
-  extra: unknown
-  /** @deprecated please use `onQueryStarted` instead */
-  requestId: string
-  /** @deprecated please use `onQueryStarted` instead */
-  context: Context
-}
-
 export interface QueryTypes<
   QueryArg,
   BaseQuery extends BaseQueryFn,
diff --git a/packages/toolkit/src/query/fetchBaseQuery.ts b/packages/toolkit/src/query/fetchBaseQuery.ts
--- a/packages/toolkit/src/query/fetchBaseQuery.ts
+++ b/packages/toolkit/src/query/fetchBaseQuery.ts
@@ -1,5 +1,5 @@
 import { joinUrls } from './utils'
-import { isPlainObject } from '@reduxjs/toolkit'
+import { isPlainObject } from './core/rtkImports'
 import type { BaseQueryApi, BaseQueryFn } from './baseQueryTypes'
 import type { MaybePromise, Override } from './tsHelpers'
 
diff --git a/packages/toolkit/src/query/index.ts b/packages/toolkit/src/query/index.ts
--- a/packages/toolkit/src/query/index.ts
+++ b/packages/toolkit/src/query/index.ts
@@ -1,3 +1,7 @@
+// This must remain here so that the `mangleErrors.cjs` build script
+// does not have to import this into each source file it rewrites.
+import { formatProdErrorMessage } from '@reduxjs/toolkit'
+
 export type {
   CombinedState,
   QueryCacheKey,
@@ -8,6 +12,7 @@ export type {
 } from './core/apiState'
 export { QueryStatus } from './core/apiState'
 export type { Api, ApiContext, ApiModules, Module } from './apiTypes'
+
 export type {
   BaseQueryApi,
   BaseQueryEnhancer,
@@ -31,7 +36,7 @@ export type {
 } from './fetchBaseQuery'
 export { retry } from './retry'
 export { setupListeners } from './core/setupListeners'
-export { skipSelector, skipToken } from './core/buildSelectors'
+export { skipToken } from './core/buildSelectors'
 export type {
   QueryResultSelectorResult,
   MutationResultSelectorResult,
diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts
--- a/packages/toolkit/src/query/react/buildHooks.ts
+++ b/packages/toolkit/src/query/react/buildHooks.ts
@@ -1,6 +1,10 @@
-import type { AnyAction, ThunkAction, ThunkDispatch } from '@reduxjs/toolkit'
+import type {
+  UnknownAction,
+  Selector,
+  ThunkAction,
+  ThunkDispatch,
+} from '@reduxjs/toolkit'
 import { createSelector } from '@reduxjs/toolkit'
-import type { Selector } from '@reduxjs/toolkit'
 import type { DependencyList } from 'react'
 import {
   useCallback,
@@ -24,30 +28,24 @@ import type {
   QueryDefinition,
   QueryArgFrom,
   ResultTypeFrom,
-} from '@reduxjs/toolkit/query'
-import type {
   QueryResultSelectorResult,
   MutationResultSelectorResult,
   SkipToken,
-} from '@reduxjs/toolkit/query'
-import type {
   QueryActionCreatorResult,
   MutationActionCreatorResult,
-} from '@reduxjs/toolkit/query'
-import type { SerializeQueryArgs } from '@reduxjs/toolkit/query'
-import { shallowEqual } from 'react-redux'
-import type { Api, ApiContext } from '@reduxjs/toolkit/query'
-import type {
+  SerializeQueryArgs,
+  Api,
+  ApiContext,
   TSHelpersId,
   TSHelpersNoInfer,
   TSHelpersOverride,
-} from '@reduxjs/toolkit/query'
-import type {
   ApiEndpointMutation,
   ApiEndpointQuery,
   CoreModule,
   PrefetchOptions,
 } from '@reduxjs/toolkit/query'
+
+import { shallowEqual } from 'react-redux'
 import type { ReactHooksModuleOptions } from './module'
 import { useStableQueryArgs } from './useSerializedStableValue'
 import type { UninitializedValue } from './constants'
@@ -55,6 +53,10 @@ import { UNINITIALIZED_VALUE } from './constants'
 import { useShallowStableValue } from './useShallowStableValue'
 import type { BaseQueryFn } from '../baseQueryTypes'
 import { defaultSerializeQueryArgs } from '../defaultSerializeQueryArgs'
+import {
+  InternalMiddlewareState,
+  SubscriptionSelectors,
+} from '../core/buildMiddleware/types'
 
 // Copy-pasted from React-Redux
 export const useIsomorphicLayoutEffect =
@@ -454,8 +456,8 @@ type UseQueryStateDefaultResult<D extends QueryDefinition<any, any, any, any>> =
       >
   > & {
     /**
-     * @deprecated will be removed in the next version
-     * please use the `isLoading`, `isFetching`, `isSuccess`, `isError`
+     * @deprecated Included for completeness, but discouraged.
+     * Please use the `isLoading`, `isFetching`, `isSuccess`, `isError`
      * and `isUninitialized` flags instead
      */
     status: QueryStatus
@@ -539,9 +541,6 @@ export type MutationTrigger<D extends MutationDefinition<any, any, any, any>> =
     (arg: QueryArgFrom<D>): MutationActionCreatorResult<D>
   }
 
-const defaultQueryStateSelector: QueryStateSelector<any, any> = (x) => x
-const defaultMutationStateSelector: MutationStateSelector<any, any> = (x) => x
-
 /**
  * Wrapper around `defaultQueryStateSelector` to be used in `useQuery`.
  * We want the initial render to already come back with
@@ -567,7 +566,7 @@ type GenericPrefetchThunk = (
   endpointName: any,
   arg: any,
   options: PrefetchOptions
-) => ThunkAction<void, any, any, AnyAction>
+) => ThunkAction<void, any, any, UnknownAction>
 
 /**
  *
@@ -581,9 +580,7 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
   api,
   moduleOptions: {
     batch,
-    useDispatch,
-    useSelector,
-    useStore,
+    hooks: { useDispatch, useSelector, useStore },
     unstable__sideEffectsInRender,
   },
   serializeQueryArgs,
@@ -654,7 +651,7 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
     endpointName: EndpointName,
     defaultOptions?: PrefetchOptions
   ) {
-    const dispatch = useDispatch<ThunkDispatch<any, any, AnyAction>>()
+    const dispatch = useDispatch<ThunkDispatch<any, any, UnknownAction>>()
     const stableDefaultOptions = useShallowStableValue(defaultOptions)
 
     return useCallback(
@@ -684,7 +681,28 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
         QueryDefinition<any, any, any, any, any>,
         Definitions
       >
-      const dispatch = useDispatch<ThunkDispatch<any, any, AnyAction>>()
+      const dispatch = useDispatch<ThunkDispatch<any, any, UnknownAction>>()
+      const subscriptionSelectorsRef = useRef<SubscriptionSelectors>()
+      if (!subscriptionSelectorsRef.current) {
+        const returnedValue = dispatch(
+          api.internalActions.internal_getRTKQSubscriptions()
+        )
+
+        if (process.env.NODE_ENV !== 'production') {
+          if (
+            typeof returnedValue !== 'object' ||
+            typeof returnedValue?.type === 'string'
+          ) {
+            throw new Error(
+              `Warning: Middleware for RTK-Query API at reducerPath "${api.reducerPath}" has not been added to the store.
+    You must add the middleware for RTK-Query to function correctly!`
+            )
+          }
+        }
+
+        subscriptionSelectorsRef.current =
+          returnedValue as unknown as SubscriptionSelectors
+      }
       const stableArg = useStableQueryArgs(
         skip ? skipToken : arg,
         // Even if the user provided a per-endpoint `serializeQueryArgs` with
@@ -708,28 +726,15 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
 
       let { queryCacheKey, requestId } = promiseRef.current || {}
 
-      // HACK Because the latest state is in the middleware, we actually
-      // dispatch an action that will be intercepted and returned.
+      // HACK We've saved the middleware subscription lookup callbacks into a ref,
+      // so we can directly check here if the subscription exists for this query.
       let currentRenderHasSubscription = false
       if (queryCacheKey && requestId) {
-        // This _should_ return a boolean, even if the types don't line up
-        const returnedValue = dispatch(
-          api.internalActions.internal_probeSubscription({
+        currentRenderHasSubscription =
+          subscriptionSelectorsRef.current.isRequestSubscribed(
             queryCacheKey,
-            requestId,
-          })
-        )
-
-        if (process.env.NODE_ENV !== 'production') {
-          if (typeof returnedValue !== 'boolean') {
-            throw new Error(
-              `Warning: Middleware for RTK-Query API at reducerPath "${api.reducerPath}" has not been added to the store.
-    You must add the middleware for RTK-Query to function correctly!`
-            )
-          }
-        }
-
-        currentRenderHasSubscription = !!returnedValue
+            requestId
+          )
       }
 
       const subscriptionRemoved =
@@ -818,7 +823,7 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
         QueryDefinition<any, any, any, any, any>,
         Definitions
       >
-      const dispatch = useDispatch<ThunkDispatch<any, any, AnyAction>>()
+      const dispatch = useDispatch<ThunkDispatch<any, any, UnknownAction>>()
 
       const [arg, setArg] = useState<any>(UNINITIALIZED_VALUE)
       const promiseRef = useRef<QueryActionCreatorResult<any> | undefined>()
@@ -918,7 +923,9 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
       const querySelector: Selector<ApiRootState, any, [any]> = useMemo(
         () =>
           selectFromResult
-            ? createSelector([selectDefaultResult], selectFromResult)
+            ? createSelector([selectDefaultResult], selectFromResult, {
+                devModeChecks: { identityFunctionCheck: 'never' },
+              })
             : selectDefaultResult,
         [selectDefaultResult, selectFromResult]
       )
@@ -981,15 +988,12 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
   }
 
   function buildMutationHook(name: string): UseMutation<any> {
-    return ({
-      selectFromResult = defaultMutationStateSelector,
-      fixedCacheKey,
-    } = {}) => {
+    return ({ selectFromResult, fixedCacheKey } = {}) => {
       const { select, initiate } = api.endpoints[name] as ApiEndpointMutation<
         MutationDefinition<any, any, any, any, any>,
         Definitions
       >
-      const dispatch = useDispatch<ThunkDispatch<any, any, AnyAction>>()
+      const dispatch = useDispatch<ThunkDispatch<any, any, UnknownAction>>()
       const [promise, setPromise] = useState<MutationActionCreatorResult<any>>()
 
       useEffect(
@@ -1011,13 +1015,16 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
       )
 
       const { requestId } = promise || {}
+      const selectDefaultResult = useMemo(
+        () => select({ fixedCacheKey, requestId: promise?.requestId }),
+        [fixedCacheKey, promise, select]
+      )
       const mutationSelector = useMemo(
         () =>
-          createSelector(
-            [select({ fixedCacheKey, requestId: promise?.requestId })],
-            selectFromResult
-          ),
-        [select, promise, selectFromResult, fixedCacheKey]
+          selectFromResult
+            ? createSelector([selectDefaultResult], selectFromResult)
+            : selectDefaultResult,
+        [selectFromResult, selectDefaultResult]
       )
 
       const currentState = useSelector(mutationSelector, shallowEqual)
diff --git a/packages/toolkit/src/query/react/index.ts b/packages/toolkit/src/query/react/index.ts
--- a/packages/toolkit/src/query/react/index.ts
+++ b/packages/toolkit/src/query/react/index.ts
@@ -1,3 +1,7 @@
+// This must remain here so that the `mangleErrors.cjs` build script
+// does not have to import this into each source file it rewrites.
+import { formatProdErrorMessage } from '@reduxjs/toolkit'
+
 import { coreModule, buildCreateApi } from '@reduxjs/toolkit/query'
 import { reactHooksModule, reactHooksModuleName } from './module'
 
diff --git a/packages/toolkit/src/query/react/module.ts b/packages/toolkit/src/query/react/module.ts
--- a/packages/toolkit/src/query/react/module.ts
+++ b/packages/toolkit/src/query/react/module.ts
@@ -22,6 +22,7 @@ import {
 } from 'react-redux'
 import type { QueryKeys } from '../core/apiState'
 import type { PrefetchOptions } from '../core/module'
+import { countObjectKeys } from '../utils/countObjectKeys'
 
 export const reactHooksModuleName = /* @__PURE__ */ Symbol()
 export type ReactHooksModule = typeof reactHooksModuleName
@@ -71,21 +72,26 @@ type RR = typeof import('react-redux')
 
 export interface ReactHooksModuleOptions {
   /**
-   * The version of the `batchedUpdates` function to be used
-   */
-  batch?: RR['batch']
-  /**
-   * The version of the `useDispatch` hook to be used
-   */
-  useDispatch?: RR['useDispatch']
-  /**
-   * The version of the `useSelector` hook to be used
+   * The hooks from React Redux to be used
    */
-  useSelector?: RR['useSelector']
+  hooks?: {
+    /**
+     * The version of the `useDispatch` hook to be used
+     */
+    useDispatch: RR['useDispatch']
+    /**
+     * The version of the `useSelector` hook to be used
+     */
+    useSelector: RR['useSelector']
+    /**
+     * The version of the `useStore` hook to be used
+     */
+    useStore: RR['useStore']
+  }
   /**
-   * The version of the `useStore` hook to be used
+   * The version of the `batchedUpdates` function to be used
    */
-  useStore?: RR['useStore']
+  batch?: RR['batch']
   /**
    * Enables performing asynchronous tasks immediately within a render.
    *
@@ -115,7 +121,13 @@ export interface ReactHooksModuleOptions {
  * const MyContext = React.createContext<ReactReduxContextValue>(null as any);
  * const customCreateApi = buildCreateApi(
  *   coreModule(),
- *   reactHooksModule({ useDispatch: createDispatchHook(MyContext) })
+ *   reactHooksModule({
+ *     hooks: {
+ *       useDispatch: createDispatchHook(MyContext),
+ *       useSelector: createSelectorHook(MyContext),
+ *       useStore: createStoreHook(MyContext)
+ *     }
+ *   })
  * );
  * ```
  *
@@ -123,63 +135,99 @@ export interface ReactHooksModuleOptions {
  */
 export const reactHooksModule = ({
   batch = rrBatch,
-  useDispatch = rrUseDispatch,
-  useSelector = rrUseSelector,
-  useStore = rrUseStore,
+  hooks = {
+    useDispatch: rrUseDispatch,
+    useSelector: rrUseSelector,
+    useStore: rrUseStore,
+  },
   unstable__sideEffectsInRender = false,
-}: ReactHooksModuleOptions = {}): Module<ReactHooksModule> => ({
-  name: reactHooksModuleName,
-  init(api, { serializeQueryArgs }, context) {
-    const anyApi = api as any as Api<
-      any,
-      Record<string, any>,
-      string,
-      string,
-      ReactHooksModule
-    >
-    const { buildQueryHooks, buildMutationHook, usePrefetch } = buildHooks({
-      api,
-      moduleOptions: {
-        batch,
-        useDispatch,
-        useSelector,
-        useStore,
-        unstable__sideEffectsInRender,
-      },
-      serializeQueryArgs,
-      context,
-    })
-    safeAssign(anyApi, { usePrefetch })
-    safeAssign(context, { batch })
-
-    return {
-      injectEndpoint(endpointName, definition) {
-        if (isQueryDefinition(definition)) {
-          const {
-            useQuery,
-            useLazyQuery,
-            useLazyQuerySubscription,
-            useQueryState,
-            useQuerySubscription,
-          } = buildQueryHooks(endpointName)
-          safeAssign(anyApi.endpoints[endpointName], {
-            useQuery,
-            useLazyQuery,
-            useLazyQuerySubscription,
-            useQueryState,
-            useQuerySubscription,
-          })
-          ;(api as any)[`use${capitalize(endpointName)}Query`] = useQuery
-          ;(api as any)[`useLazy${capitalize(endpointName)}Query`] =
-            useLazyQuery
-        } else if (isMutationDefinition(definition)) {
-          const useMutation = buildMutationHook(endpointName)
-          safeAssign(anyApi.endpoints[endpointName], {
-            useMutation,
-          })
-          ;(api as any)[`use${capitalize(endpointName)}Mutation`] = useMutation
+  ...rest
+}: ReactHooksModuleOptions = {}): Module<ReactHooksModule> => {
+  if (process.env.NODE_ENV !== 'production') {
+    const hookNames = ['useDispatch', 'useSelector', 'useStore'] as const
+    let warned = false
+    for (const hookName of hookNames) {
+      // warn for old hook options
+      if (countObjectKeys(rest) > 0) {
+        if ((rest as Partial<typeof hooks>)[hookName]) {
+          if (!warned) {
+            console.warn(
+              'As of RTK 2.0, the hooks now need to be specified as one object, provided under a `hooks` key:' +
+                '\n`reactHooksModule({ hooks: { useDispatch, useSelector, useStore } })`'
+            )
+            warned = true
+          }
         }
-      },
+        // migrate
+        // @ts-ignore
+        hooks[hookName] = rest[hookName]
+      }
+      // then make sure we have them all
+      if (typeof hooks[hookName] !== 'function') {
+        throw new Error(
+          `When using custom hooks for context, all ${
+            hookNames.length
+          } hooks need to be provided: ${hookNames.join(
+            ', '
+          )}.\nHook ${hookName} was either not provided or not a function.`
+        )
+      }
     }
-  },
-})
+  }
+
+  return {
+    name: reactHooksModuleName,
+    init(api, { serializeQueryArgs }, context) {
+      const anyApi = api as any as Api<
+        any,
+        Record<string, any>,
+        string,
+        string,
+        ReactHooksModule
+      >
+      const { buildQueryHooks, buildMutationHook, usePrefetch } = buildHooks({
+        api,
+        moduleOptions: {
+          batch,
+          hooks,
+          unstable__sideEffectsInRender,
+        },
+        serializeQueryArgs,
+        context,
+      })
+      safeAssign(anyApi, { usePrefetch })
+      safeAssign(context, { batch })
+
+      return {
+        injectEndpoint(endpointName, definition) {
+          if (isQueryDefinition(definition)) {
+            const {
+              useQuery,
+              useLazyQuery,
+              useLazyQuerySubscription,
+              useQueryState,
+              useQuerySubscription,
+            } = buildQueryHooks(endpointName)
+            safeAssign(anyApi.endpoints[endpointName], {
+              useQuery,
+              useLazyQuery,
+              useLazyQuerySubscription,
+              useQueryState,
+              useQuerySubscription,
+            })
+            ;(api as any)[`use${capitalize(endpointName)}Query`] = useQuery
+            ;(api as any)[`useLazy${capitalize(endpointName)}Query`] =
+              useLazyQuery
+          } else if (isMutationDefinition(definition)) {
+            const useMutation = buildMutationHook(endpointName)
+            safeAssign(anyApi.endpoints[endpointName], {
+              useMutation,
+            })
+            ;(api as any)[`use${capitalize(endpointName)}Mutation`] =
+              useMutation
+          }
+        },
+      }
+    },
+  }
+}
diff --git a/packages/toolkit/src/query/retry.ts b/packages/toolkit/src/query/retry.ts
--- a/packages/toolkit/src/query/retry.ts
+++ b/packages/toolkit/src/query/retry.ts
@@ -83,7 +83,7 @@ const retryWithBackoff: BaseQueryEnhancer<
     5,
     ((defaultOptions as any) || EMPTY_OPTIONS).maxRetries,
     ((extraOptions as any) || EMPTY_OPTIONS).maxRetries,
-  ].filter(x => x !== undefined)
+  ].filter((x) => x !== undefined)
   const [maxRetries] = possibleMaxRetries.slice(-1)
 
   const defaultRetryCondition: RetryConditionFunction = (_, __, { attempt }) =>
diff --git a/packages/toolkit/src/query/tsHelpers.ts b/packages/toolkit/src/query/tsHelpers.ts
--- a/packages/toolkit/src/query/tsHelpers.ts
+++ b/packages/toolkit/src/query/tsHelpers.ts
@@ -7,8 +7,8 @@ export function assertCast<T>(v: any): asserts v is T {}
 export function safeAssign<T extends object>(
   target: T,
   ...args: Array<Partial<NoInfer<T>>>
-) {
-  Object.assign(target, ...args)
+): T {
+  return Object.assign(target, ...args)
 }
 
 /**
diff --git a/packages/toolkit/src/query/utils/copyWithStructuralSharing.ts b/packages/toolkit/src/query/utils/copyWithStructuralSharing.ts
--- a/packages/toolkit/src/query/utils/copyWithStructuralSharing.ts
+++ b/packages/toolkit/src/query/utils/copyWithStructuralSharing.ts
@@ -1,4 +1,4 @@
-import { isPlainObject as _iPO } from '@reduxjs/toolkit'
+import { isPlainObject as _iPO } from '../core/rtkImports'
 
 // remove type guard
 const isPlainObject: (_: any) => boolean = _iPO
diff --git a/packages/toolkit/src/query/utils/countObjectKeys.ts b/packages/toolkit/src/query/utils/countObjectKeys.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/query/utils/countObjectKeys.ts
@@ -0,0 +1,14 @@
+// Fast method for counting an object's keys
+// without resorting to `Object.keys(obj).length
+// Will this make a big difference in perf? Probably not
+// But we can save a few allocations.
+
+export function countObjectKeys(obj: Record<any, any>) {
+  let count = 0
+
+  for (const _key in obj) {
+    count++
+  }
+
+  return count
+}
diff --git a/packages/toolkit/src/query/utils/joinUrls.ts b/packages/toolkit/src/query/utils/joinUrls.ts
--- a/packages/toolkit/src/query/utils/joinUrls.ts
+++ b/packages/toolkit/src/query/utils/joinUrls.ts
@@ -22,5 +22,5 @@ export function joinUrls(
   base = withoutTrailingSlash(base)
   url = withoutLeadingSlash(url)
 
-  return `${base}${delimiter}${url}`;
+  return `${base}${delimiter}${url}`
 }
diff --git a/packages/toolkit/src/react/index.ts b/packages/toolkit/src/react/index.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/react/index.ts
@@ -0,0 +1,6 @@
+// This must remain here so that the `mangleErrors.cjs` build script
+// does not have to import this into each source file it rewrites.
+import { formatProdErrorMessage } from '@reduxjs/toolkit'
+export * from '@reduxjs/toolkit'
+
+export { createDynamicMiddleware } from '../dynamicMiddleware/react'
diff --git a/packages/toolkit/src/serializableStateInvariantMiddleware.ts b/packages/toolkit/src/serializableStateInvariantMiddleware.ts
--- a/packages/toolkit/src/serializableStateInvariantMiddleware.ts
+++ b/packages/toolkit/src/serializableStateInvariantMiddleware.ts
@@ -1,5 +1,5 @@
-import isPlainObject from './isPlainObject'
 import type { Middleware } from 'redux'
+import { isAction, isPlainObject } from 'redux'
 import { getTimeMeasureUtils } from './utils'
 
 /**
@@ -190,88 +190,96 @@ export function createSerializableStateInvariantMiddleware(
 ): Middleware {
   if (process.env.NODE_ENV === 'production') {
     return () => (next) => (action) => next(action)
-  }
-  const {
-    isSerializable = isPlain,
-    getEntries,
-    ignoredActions = [],
-    ignoredActionPaths = ['meta.arg', 'meta.baseQueryMeta'],
-    ignoredPaths = [],
-    warnAfter = 32,
-    ignoreState = false,
-    ignoreActions = false,
-    disableCache = false,
-  } = options
-
-  const cache: WeakSet<object> | undefined =
-    !disableCache && WeakSet ? new WeakSet() : undefined
-
-  return (storeAPI) => (next) => (action) => {
-    const result = next(action)
-
-    const measureUtils = getTimeMeasureUtils(
-      warnAfter,
-      'SerializableStateInvariantMiddleware'
-    )
-
-    if (
-      !ignoreActions &&
-      !(ignoredActions.length && ignoredActions.indexOf(action.type) !== -1)
-    ) {
-      measureUtils.measureTime(() => {
-        const foundActionNonSerializableValue = findNonSerializableValue(
-          action,
-          '',
-          isSerializable,
-          getEntries,
-          ignoredActionPaths,
-          cache
-        )
+  } else {
+    const {
+      isSerializable = isPlain,
+      getEntries,
+      ignoredActions = [],
+      ignoredActionPaths = ['meta.arg', 'meta.baseQueryMeta'],
+      ignoredPaths = [],
+      warnAfter = 32,
+      ignoreState = false,
+      ignoreActions = false,
+      disableCache = false,
+    } = options
+
+    const cache: WeakSet<object> | undefined =
+      !disableCache && WeakSet ? new WeakSet() : undefined
+
+    return (storeAPI) => (next) => (action) => {
+      if (!isAction(action)) {
+        return next(action)
+      }
 
-        if (foundActionNonSerializableValue) {
-          const { keyPath, value } = foundActionNonSerializableValue
+      const result = next(action)
+
+      const measureUtils = getTimeMeasureUtils(
+        warnAfter,
+        'SerializableStateInvariantMiddleware'
+      )
 
-          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: ',
+      if (
+        !ignoreActions &&
+        !(
+          ignoredActions.length &&
+          ignoredActions.indexOf(action.type as any) !== -1
+        )
+      ) {
+        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)',
-            '\n(To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)'
+            '',
+            isSerializable,
+            getEntries,
+            ignoredActionPaths,
+            cache
           )
-        }
-      })
-    }
 
-    if (!ignoreState) {
-      measureUtils.measureTime(() => {
-        const state = storeAPI.getState()
-
-        const foundStateNonSerializableValue = findNonSerializableValue(
-          state,
-          '',
-          isSerializable,
-          getEntries,
-          ignoredPaths,
-          cache
-        )
+          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)',
+              '\n(To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)'
+            )
+          }
+        })
+      }
+
+      if (!ignoreState) {
+        measureUtils.measureTime(() => {
+          const state = storeAPI.getState()
+
+          const foundStateNonSerializableValue = findNonSerializableValue(
+            state,
+            '',
+            isSerializable,
+            getEntries,
+            ignoredPaths,
+            cache
+          )
 
-        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()
-    }
+        measureUtils.warnIfExceeded()
+      }
 
-    return result
+      return result
+    }
   }
 }
diff --git a/packages/toolkit/src/tsHelpers.ts b/packages/toolkit/src/tsHelpers.ts
--- a/packages/toolkit/src/tsHelpers.ts
+++ b/packages/toolkit/src/tsHelpers.ts
@@ -1,5 +1,12 @@
 import type { Middleware, StoreEnhancer } from 'redux'
-import type { EnhancerArray, MiddlewareArray } from './utils'
+import type { Tuple } from './utils'
+
+export function safeAssign<T extends object>(
+  target: T,
+  ...args: Array<Partial<NoInfer<T>>>
+) {
+  Object.assign(target, ...args)
+}
 
 /**
  * return True if T is `any`, otherwise return False
@@ -11,6 +18,8 @@ export type IsAny<T, True, False = never> =
   // test if we are going the left AND right path in the condition
   true | false extends (T extends never ? true : false) ? True : False
 
+export type CastAny<T, CastTo> = IsAny<T, CastTo, T>
+
 /**
  * return True if T is `unknown`, otherwise return False
  * taken from https://github.com/joonhocho/tsdef
@@ -84,7 +93,7 @@ export type ExcludeFromTuple<T, E, Acc extends unknown[] = []> = T extends [
   : Acc
 
 type ExtractDispatchFromMiddlewareTuple<
-  MiddlewareTuple extends any[],
+  MiddlewareTuple extends readonly any[],
   Acc extends {}
 > = MiddlewareTuple extends [infer Head, ...infer Tail]
   ? ExtractDispatchFromMiddlewareTuple<
@@ -93,7 +102,7 @@ type ExtractDispatchFromMiddlewareTuple<
     >
   : Acc
 
-export type ExtractDispatchExtensions<M> = M extends MiddlewareArray<
+export type ExtractDispatchExtensions<M> = M extends Tuple<
   infer MiddlewareTuple
 >
   ? ExtractDispatchFromMiddlewareTuple<MiddlewareTuple, {}>
@@ -102,7 +111,7 @@ export type ExtractDispatchExtensions<M> = M extends MiddlewareArray<
   : never
 
 type ExtractStoreExtensionsFromEnhancerTuple<
-  EnhancerTuple extends any[],
+  EnhancerTuple extends readonly any[],
   Acc extends {}
 > = EnhancerTuple extends [infer Head, ...infer Tail]
   ? ExtractStoreExtensionsFromEnhancerTuple<
@@ -111,9 +120,7 @@ type ExtractStoreExtensionsFromEnhancerTuple<
     >
   : Acc
 
-export type ExtractStoreExtensions<E> = E extends EnhancerArray<
-  infer EnhancerTuple
->
+export type ExtractStoreExtensions<E> = E extends Tuple<infer EnhancerTuple>
   ? ExtractStoreExtensionsFromEnhancerTuple<EnhancerTuple, {}>
   : E extends ReadonlyArray<StoreEnhancer>
   ? UnionToIntersection<
@@ -126,7 +133,7 @@ export type ExtractStoreExtensions<E> = E extends EnhancerArray<
   : never
 
 type ExtractStateExtensionsFromEnhancerTuple<
-  EnhancerTuple extends any[],
+  EnhancerTuple extends readonly any[],
   Acc extends {}
 > = EnhancerTuple extends [infer Head, ...infer Tail]
   ? ExtractStateExtensionsFromEnhancerTuple<
@@ -138,9 +145,7 @@ type ExtractStateExtensionsFromEnhancerTuple<
     >
   : Acc
 
-export type ExtractStateExtensions<E> = E extends EnhancerArray<
-  infer EnhancerTuple
->
+export type ExtractStateExtensions<E> = E extends Tuple<infer EnhancerTuple>
   ? ExtractStateExtensionsFromEnhancerTuple<EnhancerTuple, {}>
   : E extends ReadonlyArray<StoreEnhancer>
   ? UnionToIntersection<
@@ -161,8 +166,16 @@ export type ExtractStateExtensions<E> = E extends EnhancerArray<
  */
 export type NoInfer<T> = [T][T extends any ? 0 : never]
 
+export type NonUndefined<T> = T extends undefined ? never : T
+
 export type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
 
+export type WithRequiredProp<T, K extends keyof T> = Omit<T, K> &
+  Required<Pick<T, K>>
+
+export type WithOptionalProp<T, K extends keyof T> = Omit<T, K> &
+  Partial<Pick<T, K>>
+
 export interface TypeGuard<T> {
   (value: any): value is T
 }
@@ -188,3 +201,7 @@ export type ActionFromMatcher<M extends Matcher<any>> = M extends Matcher<
   : never
 
 export type Id<T> = { [K in keyof T]: T[K] } & {}
+
+export type Tail<T extends any[]> = T extends [any, ...infer Tail]
+  ? Tail
+  : never
diff --git a/packages/toolkit/src/uncheckedindexed.ts b/packages/toolkit/src/uncheckedindexed.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/uncheckedindexed.ts
@@ -0,0 +1,16 @@
+// inlined from https://github.com/EskiMojo14/uncheckedindexed
+// relies on remaining as a TS file, not .d.ts
+type IfMaybeUndefined<T, True, False> = [undefined] extends [T] ? True : False
+
+const testAccess = ({} as Record<string, 0>)['a']
+
+export type IfUncheckedIndexedAccess<True, False> = IfMaybeUndefined<
+  typeof testAccess,
+  True,
+  False
+>
+
+export type UncheckedIndexedAccess<T> = IfUncheckedIndexedAccess<
+  T | undefined,
+  T
+>
diff --git a/packages/toolkit/src/utils.ts b/packages/toolkit/src/utils.ts
--- a/packages/toolkit/src/utils.ts
+++ b/packages/toolkit/src/utils.ts
@@ -1,4 +1,4 @@
-import createNextState, { isDraftable } from 'immer'
+import { produce as createNextState, isDraftable } from 'immer'
 import type { Middleware, StoreEnhancer } from 'redux'
 
 export function getTimeMeasureUtils(maxDelay: number, fnName: string) {
@@ -27,92 +27,142 @@ export function delay(ms: number) {
   return new Promise((resolve) => setTimeout(resolve, ms))
 }
 
-/**
- * @public
- */
-export class MiddlewareArray<
-  Middlewares extends Middleware<any, any>[]
-> extends Array<Middlewares[number]> {
-  constructor(...items: Middlewares)
-  constructor(...args: any[]) {
-    super(...args)
-    Object.setPrototypeOf(this, MiddlewareArray.prototype)
-  }
-
-  static get [Symbol.species]() {
-    return MiddlewareArray as any
-  }
-
-  concat<AdditionalMiddlewares extends ReadonlyArray<Middleware<any, any>>>(
-    items: AdditionalMiddlewares
-  ): MiddlewareArray<[...Middlewares, ...AdditionalMiddlewares]>
-
-  concat<AdditionalMiddlewares extends ReadonlyArray<Middleware<any, any>>>(
-    ...items: AdditionalMiddlewares
-  ): MiddlewareArray<[...Middlewares, ...AdditionalMiddlewares]>
-  concat(...arr: any[]) {
-    return super.concat.apply(this, arr)
-  }
-
-  prepend<AdditionalMiddlewares extends ReadonlyArray<Middleware<any, any>>>(
-    items: AdditionalMiddlewares
-  ): MiddlewareArray<[...AdditionalMiddlewares, ...Middlewares]>
-
-  prepend<AdditionalMiddlewares extends ReadonlyArray<Middleware<any, any>>>(
-    ...items: AdditionalMiddlewares
-  ): MiddlewareArray<[...AdditionalMiddlewares, ...Middlewares]>
-
-  prepend(...arr: any[]) {
-    if (arr.length === 1 && Array.isArray(arr[0])) {
-      return new MiddlewareArray(...arr[0].concat(this))
+export function find<T>(
+  iterable: Iterable<T>,
+  comparator: (item: T) => boolean
+): T | undefined {
+  for (const entry of iterable) {
+    if (comparator(entry)) {
+      return entry
     }
-    return new MiddlewareArray(...arr.concat(this))
   }
+
+  return undefined
 }
 
-/**
- * @public
- */
-export class EnhancerArray<
-  Enhancers extends StoreEnhancer<any, any>[]
-> extends Array<Enhancers[number]> {
-  constructor(...items: Enhancers)
-  constructor(...args: any[]) {
-    super(...args)
-    Object.setPrototypeOf(this, EnhancerArray.prototype)
+export class Tuple<Items extends ReadonlyArray<unknown> = []> extends Array<
+  Items[number]
+> {
+  constructor(length: number)
+  constructor(...items: Items)
+  constructor(...items: any[]) {
+    super(...items)
+    Object.setPrototypeOf(this, Tuple.prototype)
   }
 
   static get [Symbol.species]() {
-    return EnhancerArray as any
+    return Tuple as any
   }
 
-  concat<AdditionalEnhancers extends ReadonlyArray<StoreEnhancer<any, any>>>(
-    items: AdditionalEnhancers
-  ): EnhancerArray<[...Enhancers, ...AdditionalEnhancers]>
-
-  concat<AdditionalEnhancers extends ReadonlyArray<StoreEnhancer<any, any>>>(
-    ...items: AdditionalEnhancers
-  ): EnhancerArray<[...Enhancers, ...AdditionalEnhancers]>
+  concat<AdditionalItems extends ReadonlyArray<unknown>>(
+    items: Tuple<AdditionalItems>
+  ): Tuple<[...Items, ...AdditionalItems]>
+  concat<AdditionalItems extends ReadonlyArray<unknown>>(
+    items: AdditionalItems
+  ): Tuple<[...Items, ...AdditionalItems]>
+  concat<AdditionalItems extends ReadonlyArray<unknown>>(
+    ...items: AdditionalItems
+  ): Tuple<[...Items, ...AdditionalItems]>
   concat(...arr: any[]) {
     return super.concat.apply(this, arr)
   }
 
-  prepend<AdditionalEnhancers extends ReadonlyArray<StoreEnhancer<any, any>>>(
-    items: AdditionalEnhancers
-  ): EnhancerArray<[...AdditionalEnhancers, ...Enhancers]>
-
-  prepend<AdditionalEnhancers extends ReadonlyArray<StoreEnhancer<any, any>>>(
-    ...items: AdditionalEnhancers
-  ): EnhancerArray<[...AdditionalEnhancers, ...Enhancers]>
-
+  prepend<AdditionalItems extends ReadonlyArray<unknown>>(
+    items: Tuple<AdditionalItems>
+  ): Tuple<[...AdditionalItems, ...Items]>
+  prepend<AdditionalItems extends ReadonlyArray<unknown>>(
+    items: AdditionalItems
+  ): Tuple<[...AdditionalItems, ...Items]>
+  prepend<AdditionalItems extends ReadonlyArray<unknown>>(
+    ...items: AdditionalItems
+  ): Tuple<[...AdditionalItems, ...Items]>
   prepend(...arr: any[]) {
     if (arr.length === 1 && Array.isArray(arr[0])) {
-      return new EnhancerArray(...arr[0].concat(this))
+      return new Tuple(...arr[0].concat(this))
     }
-    return new EnhancerArray(...arr.concat(this))
+    return new Tuple(...arr.concat(this))
   }
 }
 
 export function freezeDraftable<T>(val: T) {
   return isDraftable(val) ? createNextState(val, () => {}) : val
 }
+
+interface WeakMapEmplaceHandler<K extends object, V> {
+  /**
+   * Will be called to get value, if no value is currently in map.
+   */
+  insert?(key: K, map: WeakMap<K, V>): V
+  /**
+   * Will be called to update a value, if one exists already.
+   */
+  update?(previous: V, key: K, map: WeakMap<K, V>): V
+}
+
+interface MapEmplaceHandler<K, V> {
+  /**
+   * Will be called to get value, if no value is currently in map.
+   */
+  insert?(key: K, map: Map<K, V>): V
+  /**
+   * Will be called to update a value, if one exists already.
+   */
+  update?(previous: V, key: K, map: Map<K, V>): V
+}
+
+export function emplace<K, V>(
+  map: Map<K, V>,
+  key: K,
+  handler: MapEmplaceHandler<K, V>
+): V
+export function emplace<K extends object, V>(
+  map: WeakMap<K, V>,
+  key: K,
+  handler: WeakMapEmplaceHandler<K, V>
+): V
+/**
+ * Allow inserting a new value, or updating an existing one
+ * @throws if called for a key with no current value and no `insert` handler is provided
+ * @returns current value in map (after insertion/updating)
+ * ```ts
+ * // return current value if already in map, otherwise initialise to 0 and return that
+ * const num = emplace(map, key, {
+ *   insert: () => 0
+ * })
+ *
+ * // increase current value by one if already in map, otherwise initialise to 0
+ * const num = emplace(map, key, {
+ *   update: (n) => n + 1,
+ *   insert: () => 0,
+ * })
+ *
+ * // only update if value's already in the map - and increase it by one
+ * if (map.has(key)) {
+ *   const num = emplace(map, key, {
+ *     update: (n) => n + 1,
+ *   })
+ * }
+ * ```
+ *
+ * @remarks
+ * Based on https://github.com/tc39/proposal-upsert currently in Stage 2 - maybe in a few years we'll be able to replace this with direct method calls
+ */
+export function emplace<K extends object, V>(
+  map: WeakMap<K, V>,
+  key: K,
+  handler: WeakMapEmplaceHandler<K, V>
+): V {
+  if (map.has(key)) {
+    let value = map.get(key) as V
+    if (handler.update) {
+      value = handler.update(value, key, map)
+      map.set(key, value)
+    }
+    return value
+  }
+  if (!handler.insert)
+    throw new Error('No insert provided for key not already in map')
+  const inserted = handler.insert(key, map)
+  map.set(key, inserted)
+  return inserted
+}
diff --git a/packages/toolkit/tsup.config.ts b/packages/toolkit/tsup.config.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/tsup.config.ts
@@ -0,0 +1,248 @@
+import { fileURLToPath } from 'url'
+import path from 'path'
+import fs from 'fs'
+import type { BuildOptions as ESBuildOptions, Plugin } from 'esbuild'
+import type { Options as TsupOptions } from 'tsup'
+import { defineConfig } from 'tsup'
+import * as babel from '@babel/core'
+import { getBuildExtensions } from 'esbuild-extra'
+
+import { delay } from './src/utils'
+
+// No __dirname under Node ESM
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = path.dirname(__filename)
+
+const outputDir = path.join(__dirname, 'dist')
+
+export interface BuildOptions {
+  format: 'cjs' | 'esm'
+  name:
+    | 'development'
+    | 'production.min'
+    | 'legacy-esm'
+    | 'modern'
+    | 'browser'
+    | 'umd'
+    | 'umd.min'
+  minify: boolean
+  env: 'development' | 'production' | ''
+  target?:
+    | 'es2017'
+    | 'es2018'
+    | 'es2019'
+    | 'es2020'
+    | 'es2021'
+    | 'es2022'
+    | 'esnext'
+}
+
+export interface EntryPointOptions {
+  prefix: string
+  folder: string
+  entryPoint: string
+  extractionConfig: string
+  externals?: string[]
+}
+
+const buildTargets: BuildOptions[] = [
+  {
+    format: 'cjs',
+    name: 'development',
+    target: 'esnext',
+    minify: false,
+    env: 'development',
+  },
+  {
+    format: 'cjs',
+    name: 'production.min',
+    target: 'esnext',
+    minify: true,
+    env: 'production',
+  },
+  // ESM, embedded `process`: modern Webpack dev
+  {
+    format: 'esm',
+    name: 'modern',
+    target: 'esnext',
+    minify: false,
+    env: '',
+  },
+  // ESM, embedded `process`: fallback for Webpack 4,
+  // which doesn't support `exports` field or optional chaining
+  {
+    format: 'esm',
+    name: 'legacy-esm',
+    target: 'esnext',
+    minify: false,
+    env: '',
+  },
+  // ESM, pre-compiled "prod": browser prod
+  {
+    format: 'esm',
+    name: 'browser',
+    target: 'esnext',
+    minify: true,
+    env: 'production',
+  },
+]
+
+const entryPoints: EntryPointOptions[] = [
+  {
+    prefix: 'redux-toolkit',
+    folder: '',
+    entryPoint: 'src/index.ts',
+    extractionConfig: 'api-extractor.json',
+  },
+  {
+    prefix: 'redux-toolkit-react',
+    folder: 'react/',
+    entryPoint: 'src/react/index.ts',
+    extractionConfig: 'api-extractor-react.json',
+    externals: ['redux', '@reduxjs/toolkit'],
+  },
+  {
+    prefix: 'rtk-query',
+    folder: 'query',
+    entryPoint: 'src/query/index.ts',
+    extractionConfig: 'api-extractor.query.json',
+    externals: ['redux', '@reduxjs/toolkit'],
+  },
+  {
+    prefix: 'rtk-query-react',
+    folder: 'query/react',
+    entryPoint: 'src/query/react/index.ts',
+    extractionConfig: 'api-extractor.query-react.json',
+    externals: ['redux', '@reduxjs/toolkit'],
+  },
+]
+
+function writeCommonJSEntry(folder: string, prefix: string) {
+  fs.writeFileSync(
+    path.join(folder, 'index.js'),
+    `'use strict'
+if (process.env.NODE_ENV === 'production') {
+  module.exports = require('./${prefix}.production.min.cjs')
+} else {
+  module.exports = require('./${prefix}.development.cjs')
+}`
+  )
+}
+
+// Extract error strings, replace them with error codes, and write messages to a file
+const mangleErrorsTransform: Plugin = {
+  name: 'mangle-errors-plugin',
+  setup(build) {
+    const { onTransform } = getBuildExtensions(build, 'mangle-errors-plugin')
+
+    onTransform({ loaders: ['ts', 'tsx'] }, async (args) => {
+      try {
+        const res = babel.transformSync(args.code, {
+          parserOpts: {
+            plugins: ['typescript', 'jsx'],
+          },
+          plugins: [['./scripts/mangleErrors.cjs', { minify: false }]],
+        })!
+        return {
+          code: res.code!,
+          map: res.map!,
+        }
+      } catch (err) {
+        console.error('Babel mangleErrors error: ', err)
+        return null
+      }
+    })
+  },
+}
+
+export default defineConfig((options) => {
+  const configs = entryPoints
+    .map((entryPointConfig) => {
+      const artifactOptions: TsupOptions[] = buildTargets.map((buildTarget) => {
+        const { prefix, folder, entryPoint, externals } = entryPointConfig
+        const { format, minify, env, name, target } = buildTarget
+        const outputFilename = `${prefix}.${name}`
+
+        const folderSegments = [outputDir, folder]
+        if (format === 'cjs') {
+          folderSegments.push('cjs')
+        }
+
+        const outputFolder = path.join(...folderSegments)
+
+        const extension =
+          name === 'legacy-esm' ? '.js' : format === 'esm' ? '.mjs' : '.cjs'
+
+        const defineValues: Record<string, string> = {}
+
+        if (env) {
+          Object.assign(defineValues, {
+            'process.env.NODE_ENV': JSON.stringify(env),
+          })
+        }
+
+        const generateTypedefs = name === 'modern' && format === 'esm'
+
+        return {
+          entry: {
+            [outputFilename]: entryPoint,
+          },
+          format,
+          outDir: outputFolder,
+          target,
+          outExtension: () => ({ js: extension }),
+          minify,
+          sourcemap: true,
+          external: externals,
+          esbuildPlugins: [mangleErrorsTransform],
+          esbuildOptions(options) {
+            // Needed to prevent auto-replacing of process.env.NODE_ENV in all builds
+            options.platform = 'neutral'
+            // Needed to return to normal lookup behavior when platform: 'neutral'
+            options.mainFields = ['browser', 'module', 'main']
+            options.conditions = ['browser']
+          },
+
+          define: defineValues,
+          async onSuccess() {
+            if (format === 'cjs' && name === 'production.min') {
+              writeCommonJSEntry(outputFolder, prefix)
+            } else if (generateTypedefs) {
+              if (folder === '') {
+                // we need to delete the declaration file and replace it with the original source file
+                fs.rmSync(path.join(outputFolder, 'uncheckedindexed.d.ts'))
+
+                fs.copyFileSync(
+                  'src/uncheckedindexed.ts',
+                  path.join(outputFolder, 'uncheckedindexed.ts')
+                )
+              }
+              // TODO Copy/generate `.d.mts` files?
+              // const inputTypedefsFile = `${outputFilename}.d.ts`
+              // const outputTypedefsFile = `${outputFilename}.d.mts`
+              // const inputTypedefsPath = path.join(
+              //   outputFolder,
+              //   inputTypedefsFile
+              // )
+              // const outputTypedefsPath = path.join(
+              //   outputFolder,
+              //   outputTypedefsFile
+              // )
+              // while (!fs.existsSync(inputTypedefsPath)) {
+              //   // console.log(
+              //   //   'Waiting for typedefs to be generated: ' + inputTypedefsFile
+              //   // )
+              //   await delay(100)
+              // }
+              // fs.copyFileSync(inputTypedefsPath, outputTypedefsPath)
+            }
+          },
+        }
+      })
+
+      return artifactOptions
+    })
+    .flat()
+
+  return configs
+})
diff --git a/packages/toolkit/vitest.config.ts b/packages/toolkit/vitest.config.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/vitest.config.ts
@@ -0,0 +1,24 @@
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: 'jsdom',
+    setupFiles: ['./vitest.setup.js'],
+    include: ['./src/**/*.(spec|test).[jt]s?(x)'],
+    alias: {
+      '@reduxjs/toolkit/query/react': './src/query/react/index.ts', // @remap-prod-remove-line
+      '@reduxjs/toolkit/query': './src/query/index.ts', // @remap-prod-remove-line
+      '@reduxjs/toolkit/react': './src/index.ts', // @remap-prod-remove-line
+      '@reduxjs/toolkit': './src/index.ts', // @remap-prod-remove-line
+
+      // this mapping is disabled as we want `dist` imports in the tests only to be used for "type-only" imports which don't play a role for jest
+      //'^@reduxjs/toolkit/dist/(.*)$': '<rootDir>/src/*',
+      '@internal/': './src/',
+    },
+    deps: {
+      interopDefault: true,
+      inline: ['redux', '@reduxjs/toolkit'],
+    },
+  },
+})
diff --git a/packages/toolkit/jest.setup.js b/packages/toolkit/vitest.setup.js
similarity index 61%
rename from packages/toolkit/jest.setup.js
rename to packages/toolkit/vitest.setup.js
--- a/packages/toolkit/jest.setup.js
+++ b/packages/toolkit/vitest.setup.js
@@ -1,10 +1,11 @@
 //@ts-ignore
-const nodeFetch = require('node-fetch')
+import nodeFetch from 'node-fetch'
 //@ts-ignore
-global.fetch = nodeFetch
+globalThis.fetch = nodeFetch
 //@ts-ignore
-global.Request = nodeFetch.Request
-const { server } = require('./src/query/tests/mocks/server')
+globalThis.Request = nodeFetch.Request
+globalThis.Headers = nodeFetch.Headers
+import { server } from './src/query/tests/mocks/server'
 
 beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
 afterEach(() => server.resetHandlers())
diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js
--- a/website/docusaurus.config.js
+++ b/website/docusaurus.config.js
@@ -16,7 +16,7 @@ module.exports = {
           showLastUpdateTime: true,
           routeBasePath: '/',
           include: [
-            '{api,assets,introduction,rtk-query,tutorials,usage}/**/*.{md,mdx}',
+            '{api,assets,introduction,migrations,rtk-query,tutorials,usage}/**/*.{md,mdx}',
           ], // no other way to exclude node_modules
           remarkPlugins: [
             [
diff --git a/website/src/pages/errors.js b/website/src/pages/errors.js
new file mode 100644
--- /dev/null
+++ b/website/src/pages/errors.js
@@ -0,0 +1,61 @@
+import React from 'react'
+import Layout from '@theme/Layout'
+import { useLocation } from '@docusaurus/router'
+import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
+import styles from './styles.module.css'
+import errorCodes from '../../../errors.json'
+
+function Errors() {
+  const location = useLocation()
+  const context = useDocusaurusContext()
+  const { siteConfig = {} } = context
+  const errorCode = new URLSearchParams(location.search).get('code')
+  const error = errorCodes[errorCode]
+
+  return (
+    <Layout
+      title={`${siteConfig.title} - ${siteConfig.tagline}`}
+      description={siteConfig.tagline}
+    >
+      <main className={styles.mainFull}>
+        <h1>Production Error Codes</h1>
+        <p>
+          When Redux Toolkit is built and running in production, error text is
+          replaced by indexed error codes to save on bundle size. These errors
+          will provide a link to this page with more information about the error
+          below.
+        </p>
+        {error && (
+          <React.Fragment>
+            <p>
+              <strong>
+                The full text of the error you just encountered is:
+              </strong>
+            </p>
+            <code className={styles.errorDetails}>{error}</code>
+          </React.Fragment>
+        )}
+
+        <h2>All Error Codes</h2>
+        <table>
+          <thead>
+            <tr>
+              <th>Code</th>
+              <th>Message</th>
+            </tr>
+          </thead>
+          <tbody>
+            {Object.keys(errorCodes).map((code) => (
+              <tr>
+                <td>{code}</td>
+                <td>{errorCodes[code]}</td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </main>
+    </Layout>
+  )
+}
+
+export default Errors


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -30,7 +30,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        node: ['16.x']
+        node: ['18.x']
 
     steps:
       - name: Checkout repo
@@ -47,7 +47,7 @@ jobs:
 
       # Read existing version, reuse that, add a Git short hash
       - name: Set build version to Git commit
-        run: node scripts/writeGitVersion.js $(git rev-parse --short HEAD)
+        run: node scripts/writeGitVersion.mjs $(git rev-parse --short HEAD)
 
       - name: Check updated version
         run: jq .version package.json
@@ -67,7 +67,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        node: ['16.x']
+        node: ['18.x']
     steps:
       - name: Checkout repo
         uses: actions/checkout@v2
@@ -91,7 +91,7 @@ jobs:
       - name: Install build artifact
         run: yarn workspace @reduxjs/toolkit add $(pwd)/package.tgz
 
-      - run: sed -i -e /@remap-prod-remove-line/d ./tsconfig.base.json ./jest.config.js ./src/tests/*.* ./src/query/tests/*.*
+      - run: sed -i -e /@remap-prod-remove-line/d ./tsconfig.base.json ./vitest.config.ts ./src/tests/*.* ./src/query/tests/*.*
 
       - name: Run tests, against dist
         run: yarn test
@@ -104,8 +104,8 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        node: ['16.x']
-        ts: ['4.1', '4.2', '4.3', '4.4', '4.5', '4.6', '4.7', '4.8', '4.9.5', '5.0', '5.1', '5.2']
+        node: ['18.x']
+        ts: ['4.7', '4.8', '4.9', '5.0', '5.1', '5.2', '5.3']
     steps:
       - name: Checkout repo
         uses: actions/checkout@v2
@@ -133,7 +133,7 @@ jobs:
       - name: Show installed RTK versions
         run: yarn info @reduxjs/toolkit
 
-      - run: sed -i -e /@remap-prod-remove-line/d ./tsconfig.base.json ./jest.config.js ./src/tests/*.* ./src/query/tests/*.*
+      - run: sed -i -e /@remap-prod-remove-line/d ./tsconfig.base.json ./vitest.config.ts ./src/tests/*.* ./src/query/tests/*.*
 
       - name: Test types
         run: |
@@ -148,17 +148,8 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        node: ['16.x']
-        example:
-          [
-            'cra4',
-            'cra5',
-            'next',
-            'vite',
-            'node-standard',
-            'node-esm',
-            'are-the-types-wrong',
-          ]
+        node: ['18.x']
+        example: ['cra4', 'cra5', 'next', 'vite', 'node-standard', 'node-esm']
     defaults:
       run:
         working-directory: ./examples/publish-ci/${{ matrix.example }}
@@ -190,10 +181,43 @@ jobs:
         run: yarn add ./package.tgz
 
       - name: Show installed RTK versions
-        run: yarn info @reduxjs/toolkit
+        run: yarn info @reduxjs/toolkit && yarn why @reduxjs/toolkit
 
       - name: Build example
-        run: yarn build
+        run: NODE_OPTIONS=--openssl-legacy-provider yarn build
 
       - name: Run test step
         run: yarn test
+
+  are-the-types-wrong:
+    name: Check package config with are-the-types-wrong
+
+    needs: [build]
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        node: ['18.x']
+    steps:
+      - name: Checkout repo
+        uses: actions/checkout@v2
+
+      - name: Use node ${{ matrix.node }}
+        uses: actions/setup-node@v2
+        with:
+          node-version: ${{ matrix.node }}
+          cache: 'yarn'
+
+      - name: Install deps
+        run: yarn install
+
+      - uses: actions/download-artifact@v2
+        with:
+          name: package
+          path: packages/toolkit
+
+      - name: show folder
+        run: ls -l .
+
+      - name: Run are-the-types-wrong
+        run: yarn attw ./package.tgz --format table --ignore-rules false-cjs
diff --git a/examples/publish-ci/cra4/tests/playwright/rtkq.test.ts b/examples/publish-ci/cra4/tests/playwright/rtkq.test.ts
--- a/examples/publish-ci/cra4/tests/playwright/rtkq.test.ts
+++ b/examples/publish-ci/cra4/tests/playwright/rtkq.test.ts
@@ -1,6 +1,7 @@
 import { test, expect } from '@playwright/test'
 
 test('RTK / RTKQ Interactions', async ({ page }) => {
+  page.on('console', (msg) => console.log('Console message: ', msg.text()))
   await page.goto('http://localhost:3000')
 
   const counterValue = page.getByTestId('counter-value')
diff --git a/examples/publish-ci/cra5/tests/playwright/rtkq.test.ts b/examples/publish-ci/cra5/tests/playwright/rtkq.test.ts
--- a/examples/publish-ci/cra5/tests/playwright/rtkq.test.ts
+++ b/examples/publish-ci/cra5/tests/playwright/rtkq.test.ts
@@ -1,6 +1,7 @@
 import { test, expect } from '@playwright/test'
 
 test('RTK / RTKQ Interactions', async ({ page }) => {
+  page.on('console', (msg) => console.log('Console message: ', msg.text()))
   await page.goto('http://localhost:3000')
 
   const counterValue = page.getByTestId('counter-value')
diff --git a/examples/publish-ci/next/tests/playwright/rtkq.test.ts b/examples/publish-ci/next/tests/playwright/rtkq.test.ts
--- a/examples/publish-ci/next/tests/playwright/rtkq.test.ts
+++ b/examples/publish-ci/next/tests/playwright/rtkq.test.ts
@@ -1,6 +1,7 @@
 import { test, expect } from '@playwright/test'
 
 test('RTK / RTKQ Interactions', async ({ page }) => {
+  page.on('console', (msg) => console.log('Console message: ', msg.text()))
   await page.goto('http://localhost:3000')
 
   const counterValue = page.getByTestId('counter-value')
diff --git a/examples/publish-ci/vite/tests/playwright/rtkq.test.ts b/examples/publish-ci/vite/tests/playwright/rtkq.test.ts
--- a/examples/publish-ci/vite/tests/playwright/rtkq.test.ts
+++ b/examples/publish-ci/vite/tests/playwright/rtkq.test.ts
@@ -1,6 +1,7 @@
 import { test, expect } from '@playwright/test'
 
 test('RTK / RTKQ Interactions', async ({ page }) => {
+  page.on('console', (msg) => console.log('Console message: ', msg.text()))
   await page.goto('http://localhost:3000')
 
   const counterValue = page.getByTestId('counter-value')
diff --git a/packages/rtk-codemods/transforms/createReducerBuilder/createReducerBuilder.test.ts b/packages/rtk-codemods/transforms/createReducerBuilder/createReducerBuilder.test.ts
new file mode 100644
--- /dev/null
+++ b/packages/rtk-codemods/transforms/createReducerBuilder/createReducerBuilder.test.ts
@@ -0,0 +1,11 @@
+import path from 'path';
+import transform, { parser } from './index';
+
+import { runTransformTest } from '../../transformTestUtils';
+
+runTransformTest(
+  'createReducerBuilder',
+  transform,
+  parser,
+  path.join(__dirname, '__testfixtures__')
+);
diff --git a/packages/rtk-codemods/transforms/createReducerBuilder/test.js b/packages/rtk-codemods/transforms/createReducerBuilder/test.js
deleted file mode 100644
--- a/packages/rtk-codemods/transforms/createReducerBuilder/test.js
+++ /dev/null
@@ -1,10 +0,0 @@
-'use strict';
-require('ts-node').register();
-
-const { runTransformTest } = require('codemod-cli');
-
-runTransformTest({
-  name: 'createReducerBuilder',
-  path: require.resolve('./index.ts'),
-  fixtureDir: `${__dirname}/__testfixtures__/`,
-});
diff --git a/packages/rtk-codemods/transforms/createSliceBuilder/createSliceBuilder.test.ts b/packages/rtk-codemods/transforms/createSliceBuilder/createSliceBuilder.test.ts
new file mode 100644
--- /dev/null
+++ b/packages/rtk-codemods/transforms/createSliceBuilder/createSliceBuilder.test.ts
@@ -0,0 +1,6 @@
+import path from 'path';
+import transform, { parser } from './index';
+
+import { runTransformTest } from '../../transformTestUtils';
+
+runTransformTest('createSliceBuilder', transform, parser, path.join(__dirname, '__testfixtures__'));
diff --git a/packages/rtk-codemods/transforms/createSliceReducerBuilder/createSliceReducerBuilder.test.ts b/packages/rtk-codemods/transforms/createSliceReducerBuilder/createSliceReducerBuilder.test.ts
new file mode 100644
--- /dev/null
+++ b/packages/rtk-codemods/transforms/createSliceReducerBuilder/createSliceReducerBuilder.test.ts
@@ -0,0 +1,11 @@
+import path from 'path';
+import transform, { parser } from './index';
+
+import { runTransformTest } from '../../transformTestUtils';
+
+runTransformTest(
+  'createSliceReducerBuilder',
+  transform,
+  parser,
+  path.join(__dirname, '__testfixtures__')
+);
diff --git a/packages/rtk-codemods/transforms/createSliceBuilder/test.js b/packages/rtk-codemods/transforms/createSliceReducerBuilder/test.js
similarity index 82%
rename from packages/rtk-codemods/transforms/createSliceBuilder/test.js
rename to packages/rtk-codemods/transforms/createSliceReducerBuilder/test.js
--- a/packages/rtk-codemods/transforms/createSliceBuilder/test.js
+++ b/packages/rtk-codemods/transforms/createSliceReducerBuilder/test.js
@@ -3,7 +3,7 @@
 const { runTransformTest } = require('codemod-cli');
 
 runTransformTest({
-  name: 'createSliceBuilder',
+  name: 'createSliceReducerBuilder',
   path: require.resolve('./index.ts'),
   fixtureDir: `${__dirname}/__testfixtures__/`,
 });
diff --git a/packages/rtk-query-codegen-openapi/test/cli.test.ts b/packages/rtk-query-codegen-openapi/test/cli.test.ts
--- a/packages/rtk-query-codegen-openapi/test/cli.test.ts
+++ b/packages/rtk-query-codegen-openapi/test/cli.test.ts
@@ -70,7 +70,7 @@ Done
 
     expect(fromTs).toEqual(fromJs);
     expect(fromJson).toEqual(fromJs);
-  }, 25000);
+  }, 120000);
 
   test('missing parameters doesnt fail', async () => {
     const out = await cli([`./config.invalid-example.json`], __dirname);
diff --git a/packages/rtk-query-codegen-openapi/test/jest.setup.ts b/packages/rtk-query-codegen-openapi/test/jest.setup.ts
--- a/packages/rtk-query-codegen-openapi/test/jest.setup.ts
+++ b/packages/rtk-query-codegen-openapi/test/jest.setup.ts
@@ -1,3 +1,4 @@
+// @ts-ignore
 global.fetch = require('node-fetch');
 const { format } = require('prettier');
 const { server } = require('./mocks/server');
diff --git a/packages/rtk-query-codegen-openapi/test/tsconfig.json b/packages/rtk-query-codegen-openapi/test/tsconfig.json
--- a/packages/rtk-query-codegen-openapi/test/tsconfig.json
+++ b/packages/rtk-query-codegen-openapi/test/tsconfig.json
@@ -1,5 +1,6 @@
 {
   "compilerOptions": {
+    "lib": ["es2019"],
     "paths": {
       "@/*": ["./test/fixtures/*"],
       "@rtk-query/codegen-openapi": ["./src"]
diff --git a/packages/toolkit/src/dynamicMiddleware/tests/index.test.ts b/packages/toolkit/src/dynamicMiddleware/tests/index.test.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/dynamicMiddleware/tests/index.test.ts
@@ -0,0 +1,74 @@
+import type { Middleware } from 'redux'
+import { createDynamicMiddleware } from '../index'
+import { configureStore } from '../../configureStore'
+import type { BaseActionCreator, PayloadAction } from '../../createAction'
+import { createAction } from '../../createAction'
+import { isAllOf } from '../../matchers'
+
+const probeType = 'probeableMW/probe'
+
+export interface ProbeMiddleware
+  extends BaseActionCreator<number, typeof probeType> {
+  <Id extends number>(id: Id): PayloadAction<Id, typeof probeType>
+}
+
+export const probeMiddleware = createAction(probeType) as ProbeMiddleware
+
+const matchId =
+  <Id extends number>(id: Id) =>
+  (action: any): action is PayloadAction<Id> =>
+    action.payload === id
+
+export const makeProbeableMiddleware = <Id extends number>(
+  id: Id
+): Middleware<{
+  (action: PayloadAction<Id, typeof probeType>): Id
+}> => {
+  const isMiddlewareAction = isAllOf(probeMiddleware, matchId(id))
+  return (api) => (next) => (action) => {
+    if (isMiddlewareAction(action)) {
+      return id
+    }
+    return next(action)
+  }
+}
+
+const staticMiddleware = makeProbeableMiddleware(1)
+
+describe('createDynamicMiddleware', () => {
+  it('allows injecting middleware after store instantiation', () => {
+    const dynamicInstance = createDynamicMiddleware()
+    const store = configureStore({
+      reducer: () => 0,
+      middleware: (gDM) =>
+        gDM().prepend(dynamicInstance.middleware).concat(staticMiddleware),
+    })
+    // normal, pre-inject
+    expect(store.dispatch(probeMiddleware(2))).toEqual(probeMiddleware(2))
+    // static
+    expect(store.dispatch(probeMiddleware(1))).toBe(1)
+
+    // inject
+    dynamicInstance.addMiddleware(makeProbeableMiddleware(2))
+
+    // injected
+    expect(store.dispatch(probeMiddleware(2))).toBe(2)
+  })
+  it('returns dispatch when withMiddleware is dispatched', () => {
+    const dynamicInstance = createDynamicMiddleware()
+    const store = configureStore({
+      reducer: () => 0,
+      middleware: (gDM) => gDM().prepend(dynamicInstance.middleware),
+    })
+
+    // normal, pre-inject
+    expect(store.dispatch(probeMiddleware(2))).toEqual(probeMiddleware(2))
+
+    const dispatch = store.dispatch(
+      dynamicInstance.withMiddleware(makeProbeableMiddleware(2))
+    )
+    expect(dispatch).toEqual(expect.any(Function))
+
+    expect(dispatch(probeMiddleware(2))).toBe(2)
+  })
+})
diff --git a/packages/toolkit/src/dynamicMiddleware/tests/index.typetest.ts b/packages/toolkit/src/dynamicMiddleware/tests/index.typetest.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/dynamicMiddleware/tests/index.typetest.ts
@@ -0,0 +1,102 @@
+/* eslint-disable no-lone-blocks */
+import type { Action, UnknownAction, Middleware } from 'redux'
+import type { ThunkDispatch } from 'redux-thunk'
+import { createDynamicMiddleware } from '../index'
+import { configureStore } from '../../configureStore'
+import { expectExactType, expectType } from '../../tests/helpers'
+
+const untypedInstance = createDynamicMiddleware()
+
+interface AppDispatch extends ThunkDispatch<number, undefined, UnknownAction> {
+  (n: 1): 1
+}
+
+const typedInstance = createDynamicMiddleware<number, AppDispatch>()
+
+/**
+ * Test: instance typed at creation ensures middleware compatibility with store
+ */
+{
+  const store = configureStore({
+    reducer: () => '',
+    // @ts-expect-error
+    middleware: (gDM) => gDM().prepend(typedInstance.middleware),
+  })
+}
+
+declare const staticMiddleware: Middleware<(n: 1) => 1>
+
+const store = configureStore({
+  reducer: () => 0,
+  middleware: (gDM) =>
+    gDM().prepend(typedInstance.middleware).concat(staticMiddleware),
+})
+
+declare const compatibleMiddleware: Middleware<{}, number, AppDispatch>
+declare const incompatibleMiddleware: Middleware<{}, string, AppDispatch>
+
+/**
+ * Test: instance typed at creation enforces correct middleware type
+ */
+{
+  typedInstance.addMiddleware(
+    compatibleMiddleware,
+    // @ts-expect-error
+    incompatibleMiddleware
+  )
+
+  const dispatch = store.dispatch(
+    typedInstance.withMiddleware(
+      compatibleMiddleware,
+      // @ts-expect-error
+      incompatibleMiddleware
+    )
+  )
+}
+
+/**
+ * Test: withTypes() enforces correct middleware type
+ */
+{
+  const addMiddleware = untypedInstance.addMiddleware.withTypes<{
+    state: number
+    dispatch: AppDispatch
+  }>()
+
+  addMiddleware(
+    compatibleMiddleware,
+    // @ts-expect-error
+    incompatibleMiddleware
+  )
+
+  const withMiddleware = untypedInstance.withMiddleware.withTypes<{
+    state: number
+    dispatch: AppDispatch
+  }>()
+
+  const dispatch = store.dispatch(
+    withMiddleware(
+      compatibleMiddleware,
+      // @ts-expect-error
+      incompatibleMiddleware
+    )
+  )
+}
+
+declare const addedMiddleware: Middleware<(n: 2) => 2>
+
+/**
+ * Test: withMiddleware returns typed dispatch, with any applicable extensions
+ */
+{
+  const dispatch = store.dispatch(typedInstance.withMiddleware(addedMiddleware))
+
+  // standard
+  expectType<Action<string>>(dispatch({ type: 'foo' }))
+  // thunk
+  expectType<string>(dispatch(() => 'foo'))
+  // static
+  expectExactType(1 as const)(dispatch(1))
+  // added
+  expectExactType(2 as const)(dispatch(2))
+}
diff --git a/packages/toolkit/src/dynamicMiddleware/tests/react.test.tsx b/packages/toolkit/src/dynamicMiddleware/tests/react.test.tsx
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/dynamicMiddleware/tests/react.test.tsx
@@ -0,0 +1,101 @@
+import * as React from 'react'
+import { createDynamicMiddleware } from '../react'
+import { configureStore } from '../../configureStore'
+import { makeProbeableMiddleware, probeMiddleware } from './index.test'
+import { render } from '@testing-library/react'
+import type { Dispatch } from 'redux'
+import type { ReactReduxContextValue } from 'react-redux'
+import { Provider } from 'react-redux'
+
+const staticMiddleware = makeProbeableMiddleware(1)
+
+describe('createReactDynamicMiddleware', () => {
+  describe('createDispatchWithMiddlewareHook', () => {
+    it('injects middleware upon creation', () => {
+      const dynamicInstance = createDynamicMiddleware()
+      const store = configureStore({
+        reducer: () => 0,
+        middleware: (gDM) =>
+          gDM().prepend(dynamicInstance.middleware).concat(staticMiddleware),
+      })
+      // normal, pre-inject
+      expect(store.dispatch(probeMiddleware(2))).toEqual(probeMiddleware(2))
+      // static
+      expect(store.dispatch(probeMiddleware(1))).toBe(1)
+
+      const useDispatch = dynamicInstance.createDispatchWithMiddlewareHook(
+        makeProbeableMiddleware(2)
+      )
+
+      // injected
+      expect(store.dispatch(probeMiddleware(2))).toBe(2)
+    })
+
+    it('returns dispatch', () => {
+      const dynamicInstance = createDynamicMiddleware()
+      const store = configureStore({
+        reducer: () => 0,
+        middleware: (gDM) =>
+          gDM().prepend(dynamicInstance.middleware).concat(staticMiddleware),
+      })
+
+      const useDispatch = dynamicInstance.createDispatchWithMiddlewareHook(
+        makeProbeableMiddleware(2)
+      )
+
+      let dispatch: Dispatch | undefined
+      function Component() {
+        dispatch = useDispatch()
+
+        return null
+      }
+      render(<Component />, {
+        wrapper: ({ children }) => (
+          <Provider store={store}>{children}</Provider>
+        ),
+      })
+      expect(dispatch).toBe(store.dispatch)
+    })
+  })
+  describe('createDispatchWithMiddlewareHookFactory', () => {
+    it('returns the correct store dispatch', () => {
+      const dynamicInstance = createDynamicMiddleware()
+      const store = configureStore({
+        reducer: () => 0,
+        middleware: (gDM) =>
+          gDM().prepend(dynamicInstance.middleware).concat(staticMiddleware),
+      })
+      const store2 = configureStore({
+        reducer: () => '',
+        middleware: (gDM) =>
+          gDM().prepend(dynamicInstance.middleware).concat(staticMiddleware),
+      })
+
+      const context = React.createContext<ReactReduxContextValue>(null as any)
+
+      const createDispatchWithMiddlewareHook =
+        dynamicInstance.createDispatchWithMiddlewareHookFactory(context)
+
+      const useDispatch = createDispatchWithMiddlewareHook(
+        makeProbeableMiddleware(2)
+      )
+
+      let dispatch: Dispatch | undefined
+      function Component() {
+        dispatch = useDispatch()
+
+        return null
+      }
+      render(<Component />, {
+        wrapper: ({ children }) => (
+          <Provider store={store}>
+            <Provider context={context} store={store2}>
+              {children}
+            </Provider>
+          </Provider>
+        ),
+      })
+      expect(dispatch).toBe(store2.dispatch)
+    })
+  })
+})
diff --git a/packages/toolkit/src/dynamicMiddleware/tests/react.typetest.ts b/packages/toolkit/src/dynamicMiddleware/tests/react.typetest.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/dynamicMiddleware/tests/react.typetest.ts
@@ -0,0 +1,89 @@
+/* eslint-disable no-lone-blocks */
+import type { Context } from 'react'
+import type { ReactReduxContextValue } from 'react-redux'
+import type { Action, UnknownAction, Middleware } from 'redux'
+import type { ThunkDispatch } from 'redux-thunk'
+import { createDynamicMiddleware } from '../react'
+import { expectExactType, expectType } from '../../tests/helpers'
+/* eslint-disable no-lone-blocks */
+
+interface AppDispatch extends ThunkDispatch<number, undefined, UnknownAction> {
+  (n: 1): 1
+}
+
+const untypedInstance = createDynamicMiddleware()
+
+const typedInstance = createDynamicMiddleware<number, AppDispatch>()
+
+declare const compatibleMiddleware: Middleware<{}, number, AppDispatch>
+declare const incompatibleMiddleware: Middleware<{}, string, AppDispatch>
+
+declare const customContext: Context<ReactReduxContextValue>
+
+/**
+ * Test: instance typed at creation enforces correct middleware type
+ */
+{
+  const useDispatch = typedInstance.createDispatchWithMiddlewareHook(
+    compatibleMiddleware,
+    // @ts-expect-error
+    incompatibleMiddleware
+  )
+
+  const createDispatchWithMiddlewareHook =
+    typedInstance.createDispatchWithMiddlewareHookFactory(customContext)
+  const useDispatchWithContext = createDispatchWithMiddlewareHook(
+    compatibleMiddleware,
+    // @ts-expect-error
+    incompatibleMiddleware
+  )
+}
+
+/**
+ * Test: withTypes() enforces correct middleware type
+ */
+{
+  const createDispatchWithMiddlewareHook =
+    untypedInstance.createDispatchWithMiddlewareHook.withTypes<{
+      state: number
+      dispatch: AppDispatch
+    }>()
+  const useDispatch = createDispatchWithMiddlewareHook(
+    compatibleMiddleware,
+    // @ts-expect-error
+    incompatibleMiddleware
+  )
+
+  const createCustomDispatchWithMiddlewareHook = untypedInstance
+    .createDispatchWithMiddlewareHookFactory(customContext)
+    .withTypes<{
+      state: number
+      dispatch: AppDispatch
+    }>()
+  const useCustomDispatch = createCustomDispatchWithMiddlewareHook(
+    compatibleMiddleware,
+    // @ts-expect-error
+    incompatibleMiddleware
+  )
+}
+
+declare const addedMiddleware: Middleware<(n: 2) => 2>
+
+/**
+ * Test: useDispatchWithMW returns typed dispatch, with any applicable extensions
+ */
+{
+  const useDispatchWithMW =
+    typedInstance.createDispatchWithMiddlewareHook(addedMiddleware)
+  // eslint-disable-next-line react-hooks/rules-of-hooks
+  const dispatch = useDispatchWithMW()
+
+  // standard
+  expectType<Action<string>>(dispatch({ type: 'foo' }))
+  // thunk
+  expectType<string>(dispatch(() => 'foo'))
+  // static
+  expectExactType(1 as const)(dispatch(1))
+  // added
+  expectExactType(2 as const)(dispatch(2))
+}
diff --git a/packages/toolkit/src/entities/tests/entity_slice_enhancer.test.ts b/packages/toolkit/src/entities/tests/entity_slice_enhancer.test.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/entities/tests/entity_slice_enhancer.test.ts
@@ -0,0 +1,57 @@
+import { createEntityAdapter, createSlice } from "../..";
+import type { PayloadAction, Slice, SliceCaseReducers, UnknownAction } from "../..";
+import type { EntityId, EntityState, IdSelector } from "../models";
+import type { BookModel } from "./fixtures/book";
+
+describe('Entity Slice Enhancer', () => {
+  let slice: Slice<EntityState<BookModel, BookModel['id']>>;
+
+  beforeEach(() => {
+    const indieSlice = entitySliceEnhancer({
+      name: 'book',
+      selectId: (book: BookModel) => book.id
+    })
+    slice = indieSlice
+  })
+
+  it('exposes oneAdded', () => {
+    const book = {
+      id: '0',
+      title: 'Der Steppenwolf',
+      author: 'Herman Hesse'
+    }
+    const action = slice.actions.oneAdded(book)
+    const oneAdded = slice.reducer(undefined, action as UnknownAction)
+    expect(oneAdded.entities['0']).toBe(book)
+  })
+})
+
+interface EntitySliceArgs<T, Id extends EntityId> {
+  name: string
+  selectId: IdSelector<T, Id>
+  modelReducer?: SliceCaseReducers<T>
+}
+
+function entitySliceEnhancer<T, Id extends EntityId>({
+  name,
+  selectId,
+  modelReducer
+}: EntitySliceArgs<T, Id>) {
+  const modelAdapter = createEntityAdapter({
+    selectId
+  });
+
+  return createSlice({
+    name,
+    initialState: modelAdapter.getInitialState(),
+    reducers: {
+      oneAdded(state, action: PayloadAction<T>) {
+        modelAdapter.addOne(
+          state,
+          action.payload
+        )
+      },
+      ...modelReducer
+    }
+  })
+}
diff --git a/packages/toolkit/src/entities/tests/entity_state.test.ts b/packages/toolkit/src/entities/tests/entity_state.test.ts
--- a/packages/toolkit/src/entities/tests/entity_state.test.ts
+++ b/packages/toolkit/src/entities/tests/entity_state.test.ts
@@ -6,7 +6,7 @@ import { createSlice } from '../../createSlice'
 import type { BookModel } from './fixtures/book'
 
 describe('Entity State', () => {
-  let adapter: EntityAdapter<BookModel>
+  let adapter: EntityAdapter<BookModel, string>
 
   beforeEach(() => {
     adapter = createEntityAdapter({
diff --git a/packages/toolkit/src/entities/tests/sorted_state_adapter.test.ts b/packages/toolkit/src/entities/tests/sorted_state_adapter.test.ts
--- a/packages/toolkit/src/entities/tests/sorted_state_adapter.test.ts
+++ b/packages/toolkit/src/entities/tests/sorted_state_adapter.test.ts
@@ -11,8 +11,8 @@ import {
 import { createNextState } from '../..'
 
 describe('Sorted State Adapter', () => {
-  let adapter: EntityAdapter<BookModel>
-  let state: EntityState<BookModel>
+  let adapter: EntityAdapter<BookModel, string>
+  let state: EntityState<BookModel, string>
 
   beforeAll(() => {
     //eslint-disable-next-line
@@ -590,12 +590,10 @@ describe('Sorted State Adapter', () => {
       const result = createNextState(withTwo, (draft) => {
         adapter.removeAll(draft)
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {},
-          "ids": Array [],
-        }
-      `)
+      expect(result).toEqual({
+        entities: {},
+        ids: [],
+      })
     })
 
     test('addOne', () => {
@@ -603,19 +601,15 @@ describe('Sorted State Adapter', () => {
         adapter.addOne(draft, TheGreatGatsby)
       })
 
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "tgg": Object {
-              "id": "tgg",
-              "title": "The Great Gatsby",
-            },
+      expect(result).toEqual({
+        entities: {
+          tgg: {
+            id: 'tgg',
+            title: 'The Great Gatsby',
           },
-          "ids": Array [
-            "tgg",
-          ],
-        }
-      `)
+        },
+        ids: ['tgg'],
+      })
     })
 
     test('addMany', () => {
@@ -623,24 +617,19 @@ describe('Sorted State Adapter', () => {
         adapter.addMany(draft, [TheGreatGatsby, AnimalFarm])
       })
 
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "af": Object {
-              "id": "af",
-              "title": "Animal Farm",
-            },
-            "tgg": Object {
-              "id": "tgg",
-              "title": "The Great Gatsby",
-            },
+      expect(result).toEqual({
+        entities: {
+          af: {
+            id: 'af',
+            title: 'Animal Farm',
           },
-          "ids": Array [
-            "af",
-            "tgg",
-          ],
-        }
-      `)
+          tgg: {
+            id: 'tgg',
+            title: 'The Great Gatsby',
+          },
+        },
+        ids: ['af', 'tgg'],
+      })
     })
 
     test('setAll', () => {
@@ -648,24 +637,19 @@ describe('Sorted State Adapter', () => {
         adapter.setAll(draft, [TheGreatGatsby, AnimalFarm])
       })
 
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "af": Object {
-              "id": "af",
-              "title": "Animal Farm",
-            },
-            "tgg": Object {
-              "id": "tgg",
-              "title": "The Great Gatsby",
-            },
+      expect(result).toEqual({
+        entities: {
+          af: {
+            id: 'af',
+            title: 'Animal Farm',
+          },
+          tgg: {
+            id: 'tgg',
+            title: 'The Great Gatsby',
           },
-          "ids": Array [
-            "af",
-            "tgg",
-          ],
-        }
-      `)
+        },
+        ids: ['af', 'tgg'],
+      })
     })
 
     test('updateOne', () => {
@@ -678,19 +662,15 @@ describe('Sorted State Adapter', () => {
         })
       })
 
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "tgg": Object {
-              "id": "tgg",
-              "title": "A New Hope",
-            },
+      expect(result).toEqual({
+        entities: {
+          tgg: {
+            id: 'tgg',
+            title: 'A New Hope',
           },
-          "ids": Array [
-            "tgg",
-          ],
-        }
-      `)
+        },
+        ids: ['tgg'],
+      })
     })
 
     test('updateMany', () => {
@@ -713,49 +693,39 @@ describe('Sorted State Adapter', () => {
         ])
       })
 
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "aco": Object {
-              "id": "aco",
-              "title": "Third Change",
-            },
-            "tgg": Object {
-              "id": "tgg",
-              "title": "Second Change",
-            },
-            "th": Object {
-              "author": "Fourth Change",
-              "id": "th",
-              "title": "First Change",
-            },
+      expect(result).toEqual({
+        entities: {
+          aco: {
+            id: 'aco',
+            title: 'Third Change',
+          },
+          tgg: {
+            id: 'tgg',
+            title: 'Second Change',
           },
-          "ids": Array [
-            "th",
-            "tgg",
-            "aco",
-          ],
-        }
-      `)
+          th: {
+            author: 'Fourth Change',
+            id: 'th',
+            title: 'First Change',
+          },
+        },
+        ids: ['th', 'tgg', 'aco'],
+      })
     })
 
     test('upsertOne (insert)', () => {
       const result = createNextState(state, (draft) => {
         adapter.upsertOne(draft, TheGreatGatsby)
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "tgg": Object {
-              "id": "tgg",
-              "title": "The Great Gatsby",
-            },
+      expect(result).toEqual({
+        entities: {
+          tgg: {
+            id: 'tgg',
+            title: 'The Great Gatsby',
           },
-          "ids": Array [
-            "tgg",
-          ],
-        }
-      `)
+        },
+        ids: ['tgg'],
+      })
     })
 
     test('upsertOne (update)', () => {
@@ -766,19 +736,15 @@ describe('Sorted State Adapter', () => {
           title: 'A New Hope',
         })
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "tgg": Object {
-              "id": "tgg",
-              "title": "A New Hope",
-            },
+      expect(result).toEqual({
+        entities: {
+          tgg: {
+            id: 'tgg',
+            title: 'A New Hope',
           },
-          "ids": Array [
-            "tgg",
-          ],
-        }
-      `)
+        },
+        ids: ['tgg'],
+      })
     })
 
     test('upsertMany', () => {
@@ -792,43 +758,34 @@ describe('Sorted State Adapter', () => {
           AnimalFarm,
         ])
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "af": Object {
-              "id": "af",
-              "title": "Animal Farm",
-            },
-            "tgg": Object {
-              "id": "tgg",
-              "title": "A New Hope",
-            },
+      expect(result).toEqual({
+        entities: {
+          af: {
+            id: 'af',
+            title: 'Animal Farm',
           },
-          "ids": Array [
-            "tgg",
-            "af",
-          ],
-        }
-      `)
+          tgg: {
+            id: 'tgg',
+            title: 'A New Hope',
+          },
+        },
+        ids: ['tgg', 'af'],
+      })
     })
 
     test('setOne (insert)', () => {
       const result = createNextState(state, (draft) => {
         adapter.setOne(draft, TheGreatGatsby)
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "tgg": Object {
-              "id": "tgg",
-              "title": "The Great Gatsby",
-            },
+      expect(result).toEqual({
+        entities: {
+          tgg: {
+            id: 'tgg',
+            title: 'The Great Gatsby',
           },
-          "ids": Array [
-            "tgg",
-          ],
-        }
-      `)
+        },
+        ids: ['tgg'],
+      })
     })
 
     test('setOne (update)', () => {
@@ -839,19 +796,15 @@ describe('Sorted State Adapter', () => {
           title: 'Silmarillion',
         })
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "th": Object {
-              "id": "th",
-              "title": "Silmarillion",
-            },
+      expect(result).toEqual({
+        entities: {
+          th: {
+            id: 'th',
+            title: 'Silmarillion',
           },
-          "ids": Array [
-            "th",
-          ],
-        }
-      `)
+        },
+        ids: ['th'],
+      })
     })
 
     test('setMany', () => {
@@ -865,24 +818,19 @@ describe('Sorted State Adapter', () => {
           AnimalFarm,
         ])
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "af": Object {
-              "id": "af",
-              "title": "Animal Farm",
-            },
-            "th": Object {
-              "id": "th",
-              "title": "Silmarillion",
-            },
+      expect(result).toEqual({
+        entities: {
+          af: {
+            id: 'af',
+            title: 'Animal Farm',
+          },
+          th: {
+            id: 'th',
+            title: 'Silmarillion',
           },
-          "ids": Array [
-            "af",
-            "th",
-          ],
-        }
-      `)
+        },
+        ids: ['af', 'th'],
+      })
     })
 
     test('removeOne', () => {
@@ -890,19 +838,15 @@ describe('Sorted State Adapter', () => {
       const result = createNextState(withTwo, (draft) => {
         adapter.removeOne(draft, TheGreatGatsby.id)
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "af": Object {
-              "id": "af",
-              "title": "Animal Farm",
-            },
+      expect(result).toEqual({
+        entities: {
+          af: {
+            id: 'af',
+            title: 'Animal Farm',
           },
-          "ids": Array [
-            "af",
-          ],
-        }
-      `)
+        },
+        ids: ['af'],
+      })
     })
 
     test('removeMany', () => {
@@ -914,19 +858,15 @@ describe('Sorted State Adapter', () => {
       const result = createNextState(withThree, (draft) => {
         adapter.removeMany(draft, [TheGreatGatsby.id, AnimalFarm.id])
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "aco": Object {
-              "id": "aco",
-              "title": "A Clockwork Orange",
-            },
+      expect(result).toEqual({
+        entities: {
+          aco: {
+            id: 'aco',
+            title: 'A Clockwork Orange',
           },
-          "ids": Array [
-            "aco",
-          ],
-        }
-      `)
+        },
+        ids: ['aco'],
+      })
     })
   })
 })
diff --git a/packages/toolkit/src/entities/tests/state_adapter.test.ts b/packages/toolkit/src/entities/tests/state_adapter.test.ts
--- a/packages/toolkit/src/entities/tests/state_adapter.test.ts
+++ b/packages/toolkit/src/entities/tests/state_adapter.test.ts
@@ -6,7 +6,7 @@ import { createSlice } from '../../createSlice'
 import type { BookModel } from './fixtures/book'
 
 describe('createStateOperator', () => {
-  let adapter: EntityAdapter<BookModel>
+  let adapter: EntityAdapter<BookModel, string>
 
   beforeEach(() => {
     adapter = createEntityAdapter({
diff --git a/packages/toolkit/src/entities/tests/state_selectors.test.ts b/packages/toolkit/src/entities/tests/state_selectors.test.ts
--- a/packages/toolkit/src/entities/tests/state_selectors.test.ts
+++ b/packages/toolkit/src/entities/tests/state_selectors.test.ts
@@ -1,19 +1,21 @@
+import { createDraftSafeSelectorCreator } from '../../createDraftSafeSelector'
 import type { EntityAdapter, EntityState } from '../index'
 import { createEntityAdapter } from '../index'
 import type { EntitySelectors } from '../models'
 import type { BookModel } from './fixtures/book'
 import { AClockworkOrange, AnimalFarm, TheGreatGatsby } from './fixtures/book'
 import type { Selector } from 'reselect'
-import { createSelector } from 'reselect'
+import { createSelector, weakMapMemoize } from 'reselect'
+import { vi } from 'vitest'
 
 describe('Entity State Selectors', () => {
   describe('Composed Selectors', () => {
     interface State {
-      books: EntityState<BookModel>
+      books: EntityState<BookModel, string>
     }
 
-    let adapter: EntityAdapter<BookModel>
-    let selectors: EntitySelectors<BookModel, State>
+    let adapter: EntityAdapter<BookModel, string>
+    let selectors: EntitySelectors<BookModel, State, string>
     let state: State
 
     beforeEach(() => {
@@ -65,10 +67,14 @@ describe('Entity State Selectors', () => {
   })
 
   describe('Uncomposed Selectors', () => {
-    type State = EntityState<BookModel>
-
-    let adapter: EntityAdapter<BookModel>
-    let selectors: EntitySelectors<BookModel, EntityState<BookModel>>
+    type State = EntityState<BookModel, string>
+
+    let adapter: EntityAdapter<BookModel, string>
+    let selectors: EntitySelectors<
+      BookModel,
+      EntityState<BookModel, string>,
+      string
+    >
     let state: State
 
     beforeEach(() => {
@@ -98,9 +104,9 @@ describe('Entity State Selectors', () => {
     })
 
     it('should type single entity from Dictionary as entity type or undefined', () => {
-      expectType<Selector<EntityState<BookModel>, BookModel | undefined>>(
-        createSelector(selectors.selectEntities, (entities) => entities[0])
-      )
+      expectType<
+        Selector<EntityState<BookModel, string>, BookModel | undefined>
+      >(createSelector(selectors.selectEntities, (entities) => entities[0]))
     })
 
     it('should create a selector for selecting the list of models', () => {
@@ -122,6 +128,25 @@ describe('Entity State Selectors', () => {
       expect(second).toBe(AnimalFarm)
     })
   })
+  describe('custom createSelector instance', () => {
+    it('should use the custom createSelector function if provided', () => {
+      const memoizeSpy = vi.fn(
+        // test that we're allowed to pass memoizers with different options, as long as they're optional
+        <F extends (...args: any[]) => any>(fn: F, param?: boolean) => fn
+      )
+      const createCustomSelector = createDraftSafeSelectorCreator(memoizeSpy)
+
+      const adapter = createEntityAdapter({
+        selectId: (book: BookModel) => book.id,
+      })
+
+      adapter.getSelectors(undefined, { createSelector: createCustomSelector })
+
+      expect(memoizeSpy).toHaveBeenCalled()
+
+      memoizeSpy.mockClear()
+    })
+  })
 })
 
 function expectType<T>(t: T) {
diff --git a/packages/toolkit/src/entities/tests/unsorted_state_adapter.test.ts b/packages/toolkit/src/entities/tests/unsorted_state_adapter.test.ts
--- a/packages/toolkit/src/entities/tests/unsorted_state_adapter.test.ts
+++ b/packages/toolkit/src/entities/tests/unsorted_state_adapter.test.ts
@@ -10,8 +10,8 @@ import {
 import { createNextState } from '../..'
 
 describe('Unsorted State Adapter', () => {
-  let adapter: EntityAdapter<BookModel>
-  let state: EntityState<BookModel>
+  let adapter: EntityAdapter<BookModel, string>
+  let state: EntityState<BookModel, string>
 
   beforeAll(() => {
     //eslint-disable-next-line
@@ -435,12 +435,10 @@ describe('Unsorted State Adapter', () => {
       const result = createNextState(withTwo, (draft) => {
         adapter.removeAll(draft)
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {},
-          "ids": Array [],
-        }
-      `)
+      expect(result).toEqual({
+        entities: {},
+        ids: [],
+      })
     })
 
     test('addOne', () => {
@@ -448,19 +446,15 @@ describe('Unsorted State Adapter', () => {
         adapter.addOne(draft, TheGreatGatsby)
       })
 
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "tgg": Object {
-              "id": "tgg",
-              "title": "The Great Gatsby",
-            },
+      expect(result).toEqual({
+        entities: {
+          tgg: {
+            id: 'tgg',
+            title: 'The Great Gatsby',
           },
-          "ids": Array [
-            "tgg",
-          ],
-        }
-      `)
+        },
+        ids: ['tgg'],
+      })
     })
 
     test('addMany', () => {
@@ -468,24 +462,19 @@ describe('Unsorted State Adapter', () => {
         adapter.addMany(draft, [TheGreatGatsby, AnimalFarm])
       })
 
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "af": Object {
-              "id": "af",
-              "title": "Animal Farm",
-            },
-            "tgg": Object {
-              "id": "tgg",
-              "title": "The Great Gatsby",
-            },
+      expect(result).toEqual({
+        entities: {
+          af: {
+            id: 'af',
+            title: 'Animal Farm',
           },
-          "ids": Array [
-            "tgg",
-            "af",
-          ],
-        }
-      `)
+          tgg: {
+            id: 'tgg',
+            title: 'The Great Gatsby',
+          },
+        },
+        ids: ['tgg', 'af'],
+      })
     })
 
     test('setAll', () => {
@@ -493,24 +482,19 @@ describe('Unsorted State Adapter', () => {
         adapter.setAll(draft, [TheGreatGatsby, AnimalFarm])
       })
 
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "af": Object {
-              "id": "af",
-              "title": "Animal Farm",
-            },
-            "tgg": Object {
-              "id": "tgg",
-              "title": "The Great Gatsby",
-            },
+      expect(result).toEqual({
+        entities: {
+          af: {
+            id: 'af',
+            title: 'Animal Farm',
+          },
+          tgg: {
+            id: 'tgg',
+            title: 'The Great Gatsby',
           },
-          "ids": Array [
-            "tgg",
-            "af",
-          ],
-        }
-      `)
+        },
+        ids: ['tgg', 'af'],
+      })
     })
 
     test('updateOne', () => {
@@ -523,19 +507,15 @@ describe('Unsorted State Adapter', () => {
         })
       })
 
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "tgg": Object {
-              "id": "tgg",
-              "title": "A New Hope",
-            },
+      expect(result).toEqual({
+        entities: {
+          tgg: {
+            id: 'tgg',
+            title: 'A New Hope',
           },
-          "ids": Array [
-            "tgg",
-          ],
-        }
-      `)
+        },
+        ids: ['tgg'],
+      })
     })
 
     test('updateMany', () => {
@@ -558,49 +538,39 @@ describe('Unsorted State Adapter', () => {
         ])
       })
 
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "aco": Object {
-              "id": "aco",
-              "title": "Third Change",
-            },
-            "tgg": Object {
-              "id": "tgg",
-              "title": "Second Change",
-            },
-            "th": Object {
-              "author": "Fourth Change",
-              "id": "th",
-              "title": "First Change",
-            },
+      expect(result).toEqual({
+        entities: {
+          aco: {
+            id: 'aco',
+            title: 'Third Change',
+          },
+          tgg: {
+            id: 'tgg',
+            title: 'Second Change',
           },
-          "ids": Array [
-            "tgg",
-            "aco",
-            "th",
-          ],
-        }
-      `)
+          th: {
+            author: 'Fourth Change',
+            id: 'th',
+            title: 'First Change',
+          },
+        },
+        ids: ['tgg', 'aco', 'th'],
+      })
     })
 
     test('upsertOne (insert)', () => {
       const result = createNextState(state, (draft) => {
         adapter.upsertOne(draft, TheGreatGatsby)
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "tgg": Object {
-              "id": "tgg",
-              "title": "The Great Gatsby",
-            },
+      expect(result).toEqual({
+        entities: {
+          tgg: {
+            id: 'tgg',
+            title: 'The Great Gatsby',
           },
-          "ids": Array [
-            "tgg",
-          ],
-        }
-      `)
+        },
+        ids: ['tgg'],
+      })
     })
 
     test('upsertOne (update)', () => {
@@ -611,19 +581,15 @@ describe('Unsorted State Adapter', () => {
           title: 'A New Hope',
         })
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "tgg": Object {
-              "id": "tgg",
-              "title": "A New Hope",
-            },
+      expect(result).toEqual({
+        entities: {
+          tgg: {
+            id: 'tgg',
+            title: 'A New Hope',
           },
-          "ids": Array [
-            "tgg",
-          ],
-        }
-      `)
+        },
+        ids: ['tgg'],
+      })
     })
 
     test('upsertMany', () => {
@@ -637,43 +603,34 @@ describe('Unsorted State Adapter', () => {
           AnimalFarm,
         ])
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "af": Object {
-              "id": "af",
-              "title": "Animal Farm",
-            },
-            "tgg": Object {
-              "id": "tgg",
-              "title": "A New Hope",
-            },
+      expect(result).toEqual({
+        entities: {
+          af: {
+            id: 'af',
+            title: 'Animal Farm',
           },
-          "ids": Array [
-            "tgg",
-            "af",
-          ],
-        }
-      `)
+          tgg: {
+            id: 'tgg',
+            title: 'A New Hope',
+          },
+        },
+        ids: ['tgg', 'af'],
+      })
     })
 
     test('setOne (insert)', () => {
       const result = createNextState(state, (draft) => {
         adapter.setOne(draft, TheGreatGatsby)
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "tgg": Object {
-              "id": "tgg",
-              "title": "The Great Gatsby",
-            },
+      expect(result).toEqual({
+        entities: {
+          tgg: {
+            id: 'tgg',
+            title: 'The Great Gatsby',
           },
-          "ids": Array [
-            "tgg",
-          ],
-        }
-      `)
+        },
+        ids: ['tgg'],
+      })
     })
 
     test('setOne (update)', () => {
@@ -684,19 +641,15 @@ describe('Unsorted State Adapter', () => {
           title: 'Silmarillion',
         })
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "th": Object {
-              "id": "th",
-              "title": "Silmarillion",
-            },
+      expect(result).toEqual({
+        entities: {
+          th: {
+            id: 'th',
+            title: 'Silmarillion',
           },
-          "ids": Array [
-            "th",
-          ],
-        }
-      `)
+        },
+        ids: ['th'],
+      })
     })
 
     test('setMany', () => {
@@ -710,24 +663,19 @@ describe('Unsorted State Adapter', () => {
           AnimalFarm,
         ])
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "af": Object {
-              "id": "af",
-              "title": "Animal Farm",
-            },
-            "th": Object {
-              "id": "th",
-              "title": "Silmarillion",
-            },
+      expect(result).toEqual({
+        entities: {
+          af: {
+            id: 'af',
+            title: 'Animal Farm',
+          },
+          th: {
+            id: 'th',
+            title: 'Silmarillion',
           },
-          "ids": Array [
-            "th",
-            "af",
-          ],
-        }
-      `)
+        },
+        ids: ['th', 'af'],
+      })
     })
 
     test('removeOne', () => {
@@ -735,19 +683,15 @@ describe('Unsorted State Adapter', () => {
       const result = createNextState(withTwo, (draft) => {
         adapter.removeOne(draft, TheGreatGatsby.id)
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "af": Object {
-              "id": "af",
-              "title": "Animal Farm",
-            },
+      expect(result).toEqual({
+        entities: {
+          af: {
+            id: 'af',
+            title: 'Animal Farm',
           },
-          "ids": Array [
-            "af",
-          ],
-        }
-      `)
+        },
+        ids: ['af'],
+      })
     })
 
     test('removeMany', () => {
@@ -759,19 +703,15 @@ describe('Unsorted State Adapter', () => {
       const result = createNextState(withThree, (draft) => {
         adapter.removeMany(draft, [TheGreatGatsby.id, AnimalFarm.id])
       })
-      expect(result).toMatchInlineSnapshot(`
-        Object {
-          "entities": Object {
-            "aco": Object {
-              "id": "aco",
-              "title": "A Clockwork Orange",
-            },
+      expect(result).toEqual({
+        entities: {
+          aco: {
+            id: 'aco',
+            title: 'A Clockwork Orange',
           },
-          "ids": Array [
-            "aco",
-          ],
-        }
-      `)
+        },
+        ids: ['aco'],
+      })
     })
   })
 })
diff --git a/packages/toolkit/src/entities/tests/utils.spec.ts b/packages/toolkit/src/entities/tests/utils.spec.ts
--- a/packages/toolkit/src/entities/tests/utils.spec.ts
+++ b/packages/toolkit/src/entities/tests/utils.spec.ts
@@ -1,3 +1,4 @@
+import { vi } from 'vitest'
 import { AClockworkOrange } from './fixtures/book'
 
 describe('Entity utils', () => {
@@ -5,35 +6,35 @@ describe('Entity utils', () => {
     const OLD_ENV = process.env
 
     beforeEach(() => {
-      jest.resetModules() // this is important - it clears the cache
+      vi.resetModules() // this is important - it clears the cache
       process.env = { ...OLD_ENV, NODE_ENV: 'development' }
     })
 
     afterEach(() => {
       process.env = OLD_ENV
-      jest.resetAllMocks()
+      vi.resetAllMocks()
     })
 
-    it('should not warn when key does exist', () => {
-      const { selectIdValue } = require('../utils')
-      const spy = jest.spyOn(console, 'warn')
+    it('should not warn when key does exist', async () => {
+      const { selectIdValue } = await import('../utils')
+      const spy = vi.spyOn(console, 'warn')
 
       selectIdValue(AClockworkOrange, (book: any) => book.id)
       expect(spy).not.toHaveBeenCalled()
     })
 
-    it('should warn when key does not exist in dev mode', () => {
-      const { selectIdValue } = require('../utils')
-      const spy = jest.spyOn(console, 'warn')
+    it('should warn when key does not exist in dev mode', async () => {
+      const { selectIdValue } = await import('../utils')
+      const spy = vi.spyOn(console, 'warn')
 
       selectIdValue(AClockworkOrange, (book: any) => book.foo)
 
       expect(spy).toHaveBeenCalled()
     })
 
-    it('should warn when key is undefined in dev mode', () => {
-      const { selectIdValue } = require('../utils')
-      const spy = jest.spyOn(console, 'warn')
+    it('should warn when key is undefined in dev mode', async () => {
+      const { selectIdValue } = await import('../utils')
+      const spy = vi.spyOn(console, 'warn')
 
       const undefinedAClockworkOrange = { ...AClockworkOrange, id: undefined }
       selectIdValue(undefinedAClockworkOrange, (book: any) => book.id)
@@ -41,20 +42,20 @@ describe('Entity utils', () => {
       expect(spy).toHaveBeenCalled()
     })
 
-    it('should not warn when key does not exist in prod mode', () => {
+    it('should not warn when key does not exist in prod mode', async () => {
       process.env.NODE_ENV = 'production'
-      const { selectIdValue } = require('../utils')
-      const spy = jest.spyOn(console, 'warn')
+      const { selectIdValue } = await import('../utils')
+      const spy = vi.spyOn(console, 'warn')
 
       selectIdValue(AClockworkOrange, (book: any) => book.foo)
 
       expect(spy).not.toHaveBeenCalled()
     })
 
-    it('should not warn when key is undefined in prod mode', () => {
+    it('should not warn when key is undefined in prod mode', async () => {
       process.env.NODE_ENV = 'production'
-      const { selectIdValue } = require('../utils')
-      const spy = jest.spyOn(console, 'warn')
+      const { selectIdValue } = await import('../utils')
+      const spy = vi.spyOn(console, 'warn')
 
       const undefinedAClockworkOrange = { ...AClockworkOrange, id: undefined }
       selectIdValue(undefinedAClockworkOrange, (book: any) => book.id)
diff --git a/packages/toolkit/src/listenerMiddleware/tests/effectScenarios.test.ts b/packages/toolkit/src/listenerMiddleware/tests/effectScenarios.test.ts
--- a/packages/toolkit/src/listenerMiddleware/tests/effectScenarios.test.ts
+++ b/packages/toolkit/src/listenerMiddleware/tests/effectScenarios.test.ts
@@ -4,8 +4,9 @@ import {
   createSlice,
   isAnyOf,
 } from '@reduxjs/toolkit'
+import { vi } from 'vitest'
 
-import type { AnyAction, PayloadAction, Action } from '@reduxjs/toolkit'
+import type { PayloadAction } from '@reduxjs/toolkit'
 
 import { createListenerMiddleware, TaskAbortError } from '../index'
 
@@ -58,7 +59,7 @@ describe('Saga-style Effects Scenarios', () => {
 
   beforeAll(() => {
     const noop = () => {}
-    jest.spyOn(console, 'error').mockImplementation(noop)
+    vi.spyOn(console, 'error').mockImplementation(noop)
   })
 
   beforeEach(() => {
diff --git a/packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.test.ts b/packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.test.ts
--- a/packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.test.ts
+++ b/packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.test.ts
@@ -3,9 +3,12 @@ import {
   createAction,
   createSlice,
   isAnyOf,
+  isFluxStandardAction,
 } from '@reduxjs/toolkit'
+import type { Mock } from 'vitest'
+import { vi } from 'vitest'
 
-import type { AnyAction, PayloadAction, Action } from '@reduxjs/toolkit'
+import type { Action, UnknownAction, PayloadAction } from '@reduxjs/toolkit'
 
 import {
   createListenerMiddleware,
@@ -143,7 +146,7 @@ describe('createListenerMiddleware', () => {
     return new Promise((resolve) => setTimeout(resolve, ms))
   }
 
-  let reducer: jest.Mock
+  let reducer: Mock
   let listenerMiddleware = createListenerMiddleware()
   let { middleware, startListening, stopListening, clearListeners } =
     listenerMiddleware
@@ -159,7 +162,7 @@ describe('createListenerMiddleware', () => {
   type TestAction3 = ReturnType<typeof testAction3>
 
   beforeAll(() => {
-    jest.spyOn(console, 'error').mockImplementation(noop)
+    vi.spyOn(console, 'error').mockImplementation(noop)
   })
 
   beforeEach(() => {
@@ -168,7 +171,7 @@ describe('createListenerMiddleware', () => {
     startListening = listenerMiddleware.startListening
     stopListening = listenerMiddleware.stopListening
     clearListeners = listenerMiddleware.clearListeners
-    reducer = jest.fn(() => ({}))
+    reducer = vi.fn(() => ({}))
     store = configureStore({
       reducer,
       middleware: (gDM) => gDM().prepend(middleware),
@@ -196,7 +199,7 @@ describe('createListenerMiddleware', () => {
         >
 
       typedAddListener({
-        matcher: (action: AnyAction): action is AnyAction => true,
+        matcher: (action): action is Action => true,
         effect: (action, listenerApi) => {
           foundExtra = listenerApi.extra
           expectType<typeof originalExtra>(listenerApi.extra)
@@ -216,7 +219,7 @@ describe('createListenerMiddleware', () => {
 
   describe('Subscription and unsubscription', () => {
     test('directly subscribing', () => {
-      const effect = jest.fn((_: TestAction1) => {})
+      const effect = vi.fn((_: TestAction1) => {})
 
       startListening({
         actionCreator: testAction1,
@@ -234,7 +237,7 @@ describe('createListenerMiddleware', () => {
     })
 
     test('stopListening returns true if an entry has been unsubscribed, false otherwise', () => {
-      const effect = jest.fn((_: TestAction1) => {})
+      const effect = vi.fn((_: TestAction1) => {})
 
       startListening({
         actionCreator: testAction1,
@@ -246,7 +249,7 @@ describe('createListenerMiddleware', () => {
     })
 
     test('dispatch(removeListener({...})) returns true if an entry has been unsubscribed, false otherwise', () => {
-      const effect = jest.fn((_: TestAction1) => {})
+      const effect = vi.fn((_: TestAction1) => {})
 
       startListening({
         actionCreator: testAction1,
@@ -272,7 +275,7 @@ describe('createListenerMiddleware', () => {
     })
 
     test('can subscribe with a string action type', () => {
-      const effect = jest.fn((_: AnyAction) => {})
+      const effect = vi.fn((_: UnknownAction) => {})
 
       store.dispatch(
         addListener({
@@ -291,7 +294,7 @@ describe('createListenerMiddleware', () => {
     })
 
     test('can subscribe with a matcher function', () => {
-      const effect = jest.fn((_: AnyAction) => {})
+      const effect = vi.fn((_: UnknownAction) => {})
 
       const isAction1Or2 = isAnyOf(testAction1, testAction2)
 
@@ -358,7 +361,7 @@ describe('createListenerMiddleware', () => {
     })
 
     test('subscribing with the same listener will not make it trigger twice (like EventTarget.addEventListener())', () => {
-      const effect = jest.fn((_: TestAction1) => {})
+      const effect = vi.fn((_: TestAction1) => {})
 
       startListening({
         actionCreator: testAction1,
@@ -380,7 +383,7 @@ describe('createListenerMiddleware', () => {
     })
 
     test('unsubscribing via callback', () => {
-      const effect = jest.fn((_: TestAction1) => {})
+      const effect = vi.fn((_: TestAction1) => {})
 
       const unsubscribe = startListening({
         actionCreator: testAction1,
@@ -396,7 +399,7 @@ describe('createListenerMiddleware', () => {
     })
 
     test('directly unsubscribing', () => {
-      const effect = jest.fn((_: TestAction1) => {})
+      const effect = vi.fn((_: TestAction1) => {})
 
       startListening({
         actionCreator: testAction1,
@@ -417,7 +420,7 @@ describe('createListenerMiddleware', () => {
     })
 
     test('subscribing via action', () => {
-      const effect = jest.fn((_: TestAction1) => {})
+      const effect = vi.fn((_: TestAction1) => {})
 
       store.dispatch(
         addListener({
@@ -437,7 +440,7 @@ describe('createListenerMiddleware', () => {
     })
 
     test('unsubscribing via callback from dispatch', () => {
-      const effect = jest.fn((_: TestAction1) => {})
+      const effect = vi.fn((_: TestAction1) => {})
 
       const unsubscribe = store.dispatch(
         addListener({
@@ -458,7 +461,7 @@ describe('createListenerMiddleware', () => {
     })
 
     test('unsubscribing via action', () => {
-      const effect = jest.fn((_: TestAction1) => {})
+      const effect = vi.fn((_: TestAction1) => {})
 
       startListening({
         actionCreator: testAction1,
@@ -574,10 +577,10 @@ describe('createListenerMiddleware', () => {
       'add and remove listener with "%s" param correctly',
       (_, params) => {
         const effect: ListenerEffect<
-          AnyAction,
+          UnknownAction,
           typeof store.getState,
           typeof store.dispatch
-        > = jest.fn()
+        > = vi.fn()
 
         startListening({ ...params, effect } as any)
 
@@ -591,7 +594,7 @@ describe('createListenerMiddleware', () => {
       }
     )
 
-    const unforwardedActions: [string, AnyAction][] = [
+    const unforwardedActions: [string, UnknownAction][] = [
       [
         'addListener',
         addListener({ actionCreator: testAction1, effect: noop }),
@@ -719,7 +722,7 @@ describe('createListenerMiddleware', () => {
     })
 
     test('can unsubscribe via middleware api', () => {
-      const effect = jest.fn(
+      const effect = vi.fn(
         (action: TestAction1, api: ListenerEffectAPI<any, any>) => {
           if (action.payload === 'b') {
             api.unsubscribe()
@@ -920,7 +923,7 @@ describe('createListenerMiddleware', () => {
     })
 
     test('getOriginalState can only be invoked synchronously', async () => {
-      const onError = jest.fn()
+      const onError = vi.fn()
 
       const listenerMiddleware = createListenerMiddleware<CounterState>({
         onError,
@@ -970,7 +973,7 @@ describe('createListenerMiddleware', () => {
     test('by default, actions are forwarded to the store', () => {
       reducer.mockClear()
 
-      const effect = jest.fn((_: TestAction1) => {})
+      const effect = vi.fn((_: TestAction1) => {})
 
       startListening({
         actionCreator: testAction1,
@@ -1035,7 +1038,7 @@ describe('createListenerMiddleware', () => {
         },
       })
 
-      const effect = jest.fn(() => {})
+      const effect = vi.fn(() => {})
       startListening({ matcher, effect })
 
       store.dispatch(testAction1('a'))
@@ -1044,8 +1047,8 @@ describe('createListenerMiddleware', () => {
 
     test('Continues running other listeners if a predicate raises an error', () => {
       const matcher = (action: any): action is any => true
-      const firstListener = jest.fn(() => {})
-      const secondListener = jest.fn(() => {})
+      const firstListener = vi.fn(() => {})
+      const secondListener = vi.fn(() => {})
 
       startListening({
         // @ts-expect-error
@@ -1065,12 +1068,12 @@ describe('createListenerMiddleware', () => {
     })
 
     test('Notifies sync listener errors to `onError`, if provided', async () => {
-      const onError = jest.fn()
+      const onError = vi.fn()
       const listenerMiddleware = createListenerMiddleware({
         onError,
       })
       const { middleware, startListening } = listenerMiddleware
-      reducer = jest.fn(() => ({}))
+      reducer = vi.fn(() => ({}))
       store = configureStore({
         reducer,
         middleware: (gDM) => gDM().prepend(middleware),
@@ -1096,12 +1099,12 @@ describe('createListenerMiddleware', () => {
     })
 
     test('Notifies async listeners errors to `onError`, if provided', async () => {
-      const onError = jest.fn()
+      const onError = vi.fn()
       const listenerMiddleware = createListenerMiddleware({
         onError,
       })
       const { middleware, startListening } = listenerMiddleware
-      reducer = jest.fn(() => ({}))
+      reducer = vi.fn(() => ({}))
       store = configureStore({
         reducer,
         middleware: (gDM) => gDM().prepend(middleware),
@@ -1144,7 +1147,7 @@ describe('createListenerMiddleware', () => {
 
       typedAddListener({
         predicate: incrementByAmount.match,
-        async effect(_: AnyAction, listenerApi) {
+        async effect(_: UnknownAction, listenerApi) {
           result = await listenerApi.take(increment.match)
         },
       })
@@ -1412,7 +1415,7 @@ describe('createListenerMiddleware', () => {
           action,
           currentState,
           previousState
-        ): action is AnyAction => {
+        ): action is UnknownAction => {
           expectUnknown(currentState)
           expectUnknown(previousState)
           return true
@@ -1432,7 +1435,7 @@ describe('createListenerMiddleware', () => {
           action,
           currentState,
           previousState
-        ): action is AnyAction => {
+        ): action is UnknownAction => {
           expectUnknown(currentState)
           expectUnknown(previousState)
           return true
@@ -1458,7 +1461,7 @@ describe('createListenerMiddleware', () => {
             action,
             currentState,
             previousState
-          ): action is AnyAction => {
+          ): action is UnknownAction => {
             expectUnknown(currentState)
             expectUnknown(previousState)
             return true
@@ -1518,20 +1521,23 @@ describe('createListenerMiddleware', () => {
           currentState,
           previousState
         ): action is PayloadAction<number> => {
-          return typeof action.payload === 'boolean'
+          return (
+            isFluxStandardAction(action) && typeof action.payload === 'boolean'
+          )
         },
         effect: (action, listenerApi) => {
-          // @ts-expect-error
           expectExactType<PayloadAction<number>>(action)
         },
       })
 
       startListening({
         predicate: (action, currentState) => {
-          return typeof action.payload === 'number'
+          return (
+            isFluxStandardAction(action) && typeof action.payload === 'number'
+          )
         },
         effect: (action, listenerApi) => {
-          expectExactType<AnyAction>(action)
+          expectExactType<UnknownAction>(action)
         },
       })
 
@@ -1571,7 +1577,7 @@ describe('createListenerMiddleware', () => {
           action,
           currentState,
           previousState
-        ): action is AnyAction => {
+        ): action is UnknownAction => {
           expectNotAny(currentState)
           expectNotAny(previousState)
           expectExactType<CounterState>(currentState)
@@ -1592,7 +1598,7 @@ describe('createListenerMiddleware', () => {
       typedMiddleware.startListening({
         // TODO Why won't this infer the listener's `action` with implicit argument types?
         predicate: (
-          action: AnyAction,
+          action: UnknownAction,
           currentState: CounterState
         ): action is PayloadAction<number> => {
           expectNotAny(currentState)
@@ -1653,7 +1659,7 @@ describe('createListenerMiddleware', () => {
             action,
             currentState,
             previousState
-          ): action is AnyAction => {
+          ): action is UnknownAction => {
             expectNotAny(currentState)
             expectNotAny(previousState)
             expectExactType<CounterState>(currentState)
@@ -1683,7 +1689,7 @@ describe('createListenerMiddleware', () => {
           action,
           currentState,
           previousState
-        ): action is AnyAction => {
+        ): action is UnknownAction => {
           expectNotAny(currentState)
           expectNotAny(previousState)
           expectExactType<CounterState>(currentState)
@@ -1720,7 +1726,7 @@ describe('createListenerMiddleware', () => {
             action,
             currentState,
             previousState
-          ): action is AnyAction => {
+          ): action is UnknownAction => {
             expectNotAny(currentState)
             expectNotAny(previousState)
             expectExactType<CounterState>(currentState)
diff --git a/packages/toolkit/src/query/tests/apiProvider.test.tsx b/packages/toolkit/src/query/tests/apiProvider.test.tsx
--- a/packages/toolkit/src/query/tests/apiProvider.test.tsx
+++ b/packages/toolkit/src/query/tests/apiProvider.test.tsx
@@ -2,6 +2,8 @@ import * as React from 'react'
 import { createApi, ApiProvider } from '@reduxjs/toolkit/query/react'
 import { fireEvent, render, waitFor } from '@testing-library/react'
 import { waitMs } from './helpers'
+import { Provider } from 'react-redux'
+import { configureStore } from '@reduxjs/toolkit'
 
 const api = createApi({
   baseQuery: async (arg: any) => {
@@ -57,4 +59,15 @@ describe('ApiProvider', () => {
     // Being that nothing has changed in the args, this should never fire.
     expect(getByTestId('isFetching').textContent).toBe('false')
   })
+  test('ApiProvider throws if nested inside a Redux context', () => {
+    expect(() =>
+      render(
+        <Provider store={configureStore({ reducer: () => null })}>
+          <ApiProvider api={api}>child</ApiProvider>
+        </Provider>
+      )
+    ).toThrowErrorMatchingInlineSnapshot(
+      '"Existing Redux context detected. If you already have a store set up, please use the traditional Redux setup."'
+    )
+  })
 })
diff --git a/packages/toolkit/src/query/tests/buildCreateApi.test.tsx b/packages/toolkit/src/query/tests/buildCreateApi.test.tsx
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/query/tests/buildCreateApi.test.tsx
@@ -0,0 +1,126 @@
+import * as React from 'react'
+import type { ReactReduxContextValue } from 'react-redux'
+import {
+  createDispatchHook,
+  createSelectorHook,
+  createStoreHook,
+  Provider,
+} from 'react-redux'
+import {
+  buildCreateApi,
+  coreModule,
+  reactHooksModule,
+} from '@reduxjs/toolkit/query/react'
+import {
+  act,
+  fireEvent,
+  render,
+  screen,
+  waitFor,
+  renderHook,
+} from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { rest } from 'msw'
+import {
+  actionsReducer,
+  ANY,
+  expectExactType,
+  expectType,
+  setupApiStore,
+  withProvider,
+  useRenderCounter,
+  waitMs,
+} from './helpers'
+import { server } from './mocks/server'
+import type { UnknownAction } from 'redux'
+import type { SubscriptionOptions } from '@reduxjs/toolkit/dist/query/core/apiState'
+import type { SerializedError } from '@reduxjs/toolkit'
+import { createListenerMiddleware, configureStore } from '@reduxjs/toolkit'
+import { delay } from '../../utils'
+
+const MyContext = React.createContext<ReactReduxContextValue>(null as any)
+
+describe('buildCreateApi', () => {
+  test('Works with all hooks provided', async () => {
+    const customCreateApi = buildCreateApi(
+      coreModule(),
+      reactHooksModule({
+        hooks: {
+          useDispatch: createDispatchHook(MyContext),
+          useSelector: createSelectorHook(MyContext),
+          useStore: createStoreHook(MyContext),
+        },
+      })
+    )
+
+    const api = customCreateApi({
+      baseQuery: async (arg: any) => {
+        await waitMs()
+
+        return {
+          data: arg?.body ? { ...arg.body } : {},
+        }
+      },
+      endpoints: (build) => ({
+        getUser: build.query<{ name: string }, number>({
+          query: () => ({
+            body: { name: 'Timmy' },
+          }),
+        }),
+      }),
+    })
+
+    let getRenderCount: () => number = () => 0
+
+    const storeRef = setupApiStore(api, {}, { withoutTestLifecycles: true })
+
+    // Copy of 'useQuery hook basic render count assumptions' from `buildHooks.test.tsx`
+    function User() {
+      const { isFetching } = api.endpoints.getUser.useQuery(1)
+      getRenderCount = useRenderCounter()
+
+      return (
+        <div>
+          <div data-testid="isFetching">{String(isFetching)}</div>
+        </div>
+      )
+    }
+
+    function Wrapper({ children }: any) {
+      return (
+        <Provider store={storeRef.store} context={MyContext}>
+          {children}
+        </Provider>
+      )
+    }
+
+    render(<User />, { wrapper: Wrapper })
+    // By the time this runs, the initial render will happen, and the query
+    //  will start immediately running by the time we can expect this
+    expect(getRenderCount()).toBe(2)
+
+    await waitFor(() =>
+      expect(screen.getByTestId('isFetching').textContent).toBe('false')
+    )
+    expect(getRenderCount()).toBe(3)
+  })
+
+  test("Throws an error if you don't provide all hooks", async () => {
+    const callBuildCreateApi = () => {
+      const customCreateApi = buildCreateApi(
+        coreModule(),
+        reactHooksModule({
+          // @ts-ignore
+          hooks: {
+            useDispatch: createDispatchHook(MyContext),
+            useSelector: createSelectorHook(MyContext),
+          },
+        })
+      )
+    }
+
+    expect(callBuildCreateApi).toThrowErrorMatchingInlineSnapshot(
+      `"When using custom hooks for context, all 3 hooks need to be provided: useDispatch, useSelector, useStore.\nHook useStore was either not provided or not a function."`
+    )
+  })
+})
diff --git a/packages/toolkit/src/query/tests/buildHooks.test.tsx b/packages/toolkit/src/query/tests/buildHooks.test.tsx
--- a/packages/toolkit/src/query/tests/buildHooks.test.tsx
+++ b/packages/toolkit/src/query/tests/buildHooks.test.tsx
@@ -1,4 +1,6 @@
 import * as React from 'react'
+import type { SpyInstance } from 'vitest'
+import { vi } from 'vitest'
 import type {
   UseMutation,
   UseQuery,
@@ -30,11 +32,13 @@ import {
   waitMs,
 } from './helpers'
 import { server } from './mocks/server'
-import type { AnyAction } from 'redux'
+import type { UnknownAction } from 'redux'
 import type { SubscriptionOptions } from '@reduxjs/toolkit/dist/query/core/apiState'
 import type { SerializedError } from '@reduxjs/toolkit'
 import { createListenerMiddleware, configureStore } from '@reduxjs/toolkit'
 import { delay } from '../../utils'
+import type { SubscriptionSelectors } from '../core/buildMiddleware/types'
+import { countObjectKeys } from '../utils/countObjectKeys'
 
 // Just setup a temporary in-memory counter for tests that `getIncrementedAmount`.
 // This can be used to test how many renders happen due to data changes or
@@ -124,7 +128,7 @@ const api = createApi({
 
 const listenerMiddleware = createListenerMiddleware()
 
-let actions: AnyAction[] = []
+let actions: UnknownAction[] = []
 
 const storeRef = setupApiStore(
   api,
@@ -136,6 +140,9 @@ const storeRef = setupApiStore(
   }
 )
 
+let getSubscriptions: SubscriptionSelectors['getSubscriptions']
+let getSubscriptionCount: SubscriptionSelectors['getSubscriptionCount']
+
 beforeEach(() => {
   actions = []
   listenerMiddleware.startListening({
@@ -144,6 +151,9 @@ beforeEach(() => {
       actions.push(action)
     },
   })
+  ;({ getSubscriptions, getSubscriptionCount } = storeRef.store.dispatch(
+    api.internalActions.internal_getRTKQSubscriptions()
+  ) as unknown as SubscriptionSelectors)
 })
 
 afterEach(() => {
@@ -675,7 +685,7 @@ describe('hooks tests', () => {
         )
       })
       test('with `selectFromResult`', async () => {
-        const selectFromResult = jest.fn((x) => x)
+        const selectFromResult = vi.fn((x) => x)
         const { result } = renderHook(
           () => api.endpoints.getUser.useQuery(5, { selectFromResult }),
           {
@@ -736,14 +746,13 @@ describe('hooks tests', () => {
         withoutTestLifecycles: true,
       })
 
-      const getSubscriptions = () => storeRef.store.getState().api.subscriptions
-
       const checkNumSubscriptions = (arg: string, count: number) => {
         const subscriptions = getSubscriptions()
         const cacheKeyEntry = subscriptions[arg]
 
         if (cacheKeyEntry) {
-          expect(Object.values(cacheKeyEntry).length).toBe(count)
+          const subscriptionCount = Object.keys(cacheKeyEntry) //getSubscriptionCount(arg)
+          expect(subscriptionCount).toBe(count)
         }
       }
 
@@ -809,10 +818,10 @@ describe('hooks tests', () => {
     })
 
     describe('Hook middleware requirements', () => {
-      let mock: jest.SpyInstance
+      let mock: SpyInstance
 
       beforeEach(() => {
-        mock = jest.spyOn(console, 'error').mockImplementation(() => {})
+        mock = vi.spyOn(console, 'error').mockImplementation(() => {})
       })
 
       afterEach(() => {
@@ -1343,7 +1352,6 @@ describe('hooks tests', () => {
           expectType<() => void>(res.abort)
           expectType<() => Promise<{ name: string }>>(res.unwrap)
           expectType<() => void>(res.reset)
-          expectType<() => void>(res.unsubscribe)
 
           // abort the mutation immediately to force an error
           res.abort()
@@ -1425,25 +1433,19 @@ describe('hooks tests', () => {
 
       await screen.findByText(/isUninitialized/i)
       expect(screen.queryByText('Yay')).toBeNull()
-      expect(Object.keys(storeRef.store.getState().api.mutations).length).toBe(
-        0
-      )
+      expect(countObjectKeys(storeRef.store.getState().api.mutations)).toBe(0)
 
       userEvent.click(screen.getByRole('button', { name: 'trigger' }))
 
       await screen.findByText(/isSuccess/i)
       expect(screen.queryByText('Yay')).not.toBeNull()
-      expect(Object.keys(storeRef.store.getState().api.mutations).length).toBe(
-        1
-      )
+      expect(countObjectKeys(storeRef.store.getState().api.mutations)).toBe(1)
 
       userEvent.click(screen.getByRole('button', { name: 'reset' }))
 
       await screen.findByText(/isUninitialized/i)
       expect(screen.queryByText('Yay')).toBeNull()
-      expect(Object.keys(storeRef.store.getState().api.mutations).length).toBe(
-        0
-      )
+      expect(countObjectKeys(storeRef.store.getState().api.mutations)).toBe(0)
     })
   })
 
@@ -1746,11 +1748,7 @@ describe('hooks tests', () => {
       }),
     })
 
-    const storeRef = setupApiStore(api, {
-      actions(state: AnyAction[] = [], action: AnyAction) {
-        return [...state, action]
-      },
-    })
+    const storeRef = setupApiStore(api, { ...actionsReducer })
     test('initially failed useQueries that provide an tag will refetch after a mutation invalidates it', async () => {
       const checkSessionData = { name: 'matt' }
       server.use(
@@ -1817,7 +1815,6 @@ describe('hooks tests', () => {
       expect(storeRef.store.getState().actions).toMatchSequence(
         api.internalActions.middlewareRegistered.match,
         checkSession.matchPending,
-        api.internalActions.subscriptionsUpdated.match,
         checkSession.matchRejected,
         login.matchPending,
         login.matchFulfilled,
@@ -2537,11 +2534,6 @@ describe('skip behaviour', () => {
     isUninitialized: true,
   }
 
-  function subscriptionCount(key: string) {
-    return Object.keys(storeRef.store.getState().api.subscriptions[key] || {})
-      .length
-  }
-
   test('normal skip', async () => {
     const { result, rerender } = renderHook(
       ([arg, options]: Parameters<typeof api.endpoints.getUser.useQuery>) =>
@@ -2554,14 +2546,14 @@ describe('skip behaviour', () => {
 
     expect(result.current).toEqual(uninitialized)
     await delay(1)
-    expect(subscriptionCount('getUser(1)')).toBe(0)
+    expect(getSubscriptionCount('getUser(1)')).toBe(0)
 
     await act(async () => {
       rerender([1])
     })
     expect(result.current).toMatchObject({ status: QueryStatus.fulfilled })
     await delay(1)
-    expect(subscriptionCount('getUser(1)')).toBe(1)
+    expect(getSubscriptionCount('getUser(1)')).toBe(1)
 
     await act(async () => {
       rerender([1, { skip: true }])
@@ -2572,7 +2564,7 @@ describe('skip behaviour', () => {
       data: { name: 'Timmy' },
     })
     await delay(1)
-    expect(subscriptionCount('getUser(1)')).toBe(0)
+    expect(getSubscriptionCount('getUser(1)')).toBe(0)
   })
 
   test('skipToken', async () => {
@@ -2588,17 +2580,17 @@ describe('skip behaviour', () => {
     expect(result.current).toEqual(uninitialized)
     await delay(1)
 
-    expect(subscriptionCount('getUser(1)')).toBe(0)
+    expect(getSubscriptionCount('getUser(1)')).toBe(0)
     // also no subscription on `getUser(skipToken)` or similar:
-    expect(storeRef.store.getState().api.subscriptions).toEqual({})
+    expect(getSubscriptions()).toEqual({})
 
     await act(async () => {
       rerender([1])
     })
     expect(result.current).toMatchObject({ status: QueryStatus.fulfilled })
     await delay(1)
-    expect(subscriptionCount('getUser(1)')).toBe(1)
-    expect(storeRef.store.getState().api.subscriptions).not.toEqual({})
+    expect(getSubscriptionCount('getUser(1)')).toBe(1)
+    expect(getSubscriptions()).not.toEqual({})
 
     await act(async () => {
       rerender([skipToken])
@@ -2609,7 +2601,7 @@ describe('skip behaviour', () => {
       data: { name: 'Timmy' },
     })
     await delay(1)
-    expect(subscriptionCount('getUser(1)')).toBe(0)
+    expect(getSubscriptionCount('getUser(1)')).toBe(0)
   })
 
   test('skipping a previously fetched query retains the existing value as `data`, but clears `currentData`', async () => {
diff --git a/packages/toolkit/src/query/tests/buildInitiate.test.tsx b/packages/toolkit/src/query/tests/buildInitiate.test.tsx
--- a/packages/toolkit/src/query/tests/buildInitiate.test.tsx
+++ b/packages/toolkit/src/query/tests/buildInitiate.test.tsx
@@ -1,4 +1,5 @@
 import { createApi } from '../core'
+import type { SubscriptionSelectors } from '../core/buildMiddleware/types'
 import { fakeBaseQuery } from '../fakeBaseQuery'
 import { setupApiStore } from './helpers'
 
@@ -13,11 +14,26 @@ const api = createApi({
         return { data }
       },
     }),
+    failing: build.query<void, void>({
+      async queryFn() {
+        await Promise.resolve()
+        return { error: { status: 500, data: 'error' } }
+      },
+    }),
   }),
 })
 
 const storeRef = setupApiStore(api)
 
+let getSubscriptions: SubscriptionSelectors['getSubscriptions']
+let isRequestSubscribed: SubscriptionSelectors['isRequestSubscribed']
+
+beforeEach(() => {
+  ;({ getSubscriptions, isRequestSubscribed } = storeRef.store.dispatch(
+    api.internalActions.internal_getRTKQSubscriptions()
+  ) as unknown as SubscriptionSelectors)
+})
+
 test('multiple synchonrous initiate calls with pre-existing cache entry', async () => {
   const { store, api } = storeRef
   // seed the store
@@ -52,3 +68,54 @@ test('multiple synchonrous initiate calls with pre-existing cache entry', async
     requestId: thirdValue.requestId,
   })
 })
+
+describe('calling initiate without a cache entry, with subscribe: false still returns correct values', () => {
+  test('successful query', async () => {
+    const { store, api } = storeRef
+    calls = 0
+    const promise = store.dispatch(
+      api.endpoints.increment.initiate(undefined, { subscribe: false })
+    )
+    expect(isRequestSubscribed('increment(undefined)', promise.requestId)).toBe(
+      false
+    )
+    expect(
+      isRequestSubscribed(
+        'increment(undefined)',
+        `${promise.requestId}_running`
+      )
+    ).toBe(true)
+
+    await expect(promise).resolves.toMatchObject({
+      data: 0,
+      status: 'fulfilled',
+    })
+    expect(
+      isRequestSubscribed(
+        'increment(undefined)',
+        `${promise.requestId}_running`
+      )
+    ).toBe(false)
+  })
+
+  test('rejected query', async () => {
+    const { store, api } = storeRef
+    calls = 0
+    const promise = store.dispatch(
+      api.endpoints.failing.initiate(undefined, { subscribe: false })
+    )
+    expect(isRequestSubscribed('failing(undefined)', promise.requestId)).toBe(
+      false
+    )
+    expect(
+      isRequestSubscribed('failing(undefined)', `${promise.requestId}_running`)
+    ).toBe(true)
+
+    await expect(promise).resolves.toMatchObject({
+      status: 'rejected',
+    })
+    expect(
+      isRequestSubscribed('failing(undefined)', `${promise.requestId}_running`)
+    ).toBe(false)
+  })
+})
diff --git a/packages/toolkit/src/query/tests/buildMiddleware.test.tsx b/packages/toolkit/src/query/tests/buildMiddleware.test.tsx
--- a/packages/toolkit/src/query/tests/buildMiddleware.test.tsx
+++ b/packages/toolkit/src/query/tests/buildMiddleware.test.tsx
@@ -37,7 +37,6 @@ it('invalidates the specified tags', async () => {
   expect(storeRef.store.getState().actions).toMatchSequence(
     api.internalActions.middlewareRegistered.match,
     getBanana.matchPending,
-    api.internalActions.subscriptionsUpdated.match,
     getBanana.matchFulfilled
   )
 
@@ -49,7 +48,6 @@ it('invalidates the specified tags', async () => {
   const firstSequence = [
     api.internalActions.middlewareRegistered.match,
     getBanana.matchPending,
-    api.internalActions.subscriptionsUpdated.match,
     getBanana.matchFulfilled,
     api.util.invalidateTags.match,
     getBanana.matchPending,
@@ -65,7 +63,6 @@ it('invalidates the specified tags', async () => {
   expect(storeRef.store.getState().actions).toMatchSequence(
     ...firstSequence,
     getBread.matchPending,
-    api.internalActions.subscriptionsUpdated.match,
     getBread.matchFulfilled,
     api.util.invalidateTags.match,
     getBread.matchPending,
diff --git a/packages/toolkit/src/query/tests/buildSelector.test.ts b/packages/toolkit/src/query/tests/buildSelector.test.ts
--- a/packages/toolkit/src/query/tests/buildSelector.test.ts
+++ b/packages/toolkit/src/query/tests/buildSelector.test.ts
@@ -52,4 +52,37 @@ describe('buildSelector', () => {
     const upperTitle = todoTitle.toUpperCase()
     expectExactType<string>(upperTitle)
   })
+  test.skip('selectCachedArgsForQuery typetest', () => {
+    interface Todo {
+      userId: number
+      id: number
+      title: string
+      completed: boolean
+    }
+
+    type Todos = Array<Todo>
+
+    const exampleApi = createApi({
+      reducerPath: 'api',
+      baseQuery: fetchBaseQuery({
+        baseUrl: 'https://jsonplaceholder.typicode.com',
+      }),
+      endpoints: (build) => ({
+        getTodos: build.query<Todos, string>({
+          query: () => '/todos',
+        }),
+      }),
+    })
+
+    const store = configureStore({
+      reducer: {
+        [exampleApi.reducerPath]: exampleApi.reducer,
+        other: () => 1,
+      },
+    })
+
+    expectExactType<string[]>(
+      exampleApi.util.selectCachedArgsForQuery(store.getState(), 'getTodos')
+    )
+  })
 })
diff --git a/packages/toolkit/src/query/tests/buildSlice.test.ts b/packages/toolkit/src/query/tests/buildSlice.test.ts
--- a/packages/toolkit/src/query/tests/buildSlice.test.ts
+++ b/packages/toolkit/src/query/tests/buildSlice.test.ts
@@ -51,6 +51,7 @@ describe('buildSlice', () => {
       api: {
         config: {
           focused: true,
+          invalidationBehavior: 'delayed',
           keepUnusedDataFor: 60,
           middlewareRegistered: true,
           online: true,
@@ -75,8 +76,8 @@ describe('buildSlice', () => {
             status: 'fulfilled',
           },
         },
-        // Filled in a tick later
-        subscriptions: expect.any(Object),
+        // Filled some time later
+        subscriptions: {},
       },
       auth: {
         token: '1234',
@@ -85,18 +86,6 @@ describe('buildSlice', () => {
 
     expect(storeRef.store.getState()).toEqual(initialQueryState)
 
-    await delay(1)
-
-    expect(storeRef.store.getState()).toEqual({
-      ...initialQueryState,
-      api: {
-        ...initialQueryState.api,
-        subscriptions: {
-          'getUser(1)': expect.any(Object),
-        },
-      },
-    })
-
     storeRef.store.dispatch(api.util.resetApiState())
 
     expect(storeRef.store.getState()).toEqual(initialState)
diff --git a/packages/toolkit/src/query/tests/buildThunks.test.tsx b/packages/toolkit/src/query/tests/buildThunks.test.tsx
--- a/packages/toolkit/src/query/tests/buildThunks.test.tsx
+++ b/packages/toolkit/src/query/tests/buildThunks.test.tsx
@@ -1,4 +1,5 @@
 import { configureStore } from '@reduxjs/toolkit'
+import { vi } from 'vitest'
 import { createApi } from '@reduxjs/toolkit/query/react'
 import { renderHook, waitFor } from '@testing-library/react'
 import type { BaseQueryApi } from '../baseQueryTypes'
@@ -86,7 +87,7 @@ describe('re-triggering behavior on arg change', () => {
     middleware: (gDM) => gDM().concat(api.middleware),
   })
 
-  const spy = jest.spyOn(getUser, 'initiate')
+  const spy = vi.spyOn(getUser, 'initiate')
   beforeEach(() => void spy.mockClear())
 
   test('re-trigger on literal value change', async () => {
@@ -101,7 +102,7 @@ describe('re-triggering behavior on arg change', () => {
     await waitFor(() => {
       expect(result.current.status).not.toBe('pending')
     })
-    
+
     expect(spy).toHaveBeenCalledTimes(1)
 
     for (let x = 1; x < 3; x++) {
diff --git a/packages/toolkit/src/query/tests/cacheCollection.test.ts b/packages/toolkit/src/query/tests/cacheCollection.test.ts
--- a/packages/toolkit/src/query/tests/cacheCollection.test.ts
+++ b/packages/toolkit/src/query/tests/cacheCollection.test.ts
@@ -1,17 +1,18 @@
 import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
 import { configureStore } from '@reduxjs/toolkit'
-import { waitMs } from './helpers'
+import { vi } from 'vitest'
 import type { Middleware, Reducer } from 'redux'
 import {
   THIRTY_TWO_BIT_MAX_INT,
   THIRTY_TWO_BIT_MAX_TIMER_SECONDS,
 } from '../core/buildMiddleware/cacheCollection'
+import { countObjectKeys } from '../utils/countObjectKeys'
 
 beforeAll(() => {
-  jest.useFakeTimers('legacy')
+  vi.useFakeTimers()
 })
 
-const onCleanup = jest.fn()
+const onCleanup = vi.fn()
 
 beforeEach(() => {
   onCleanup.mockClear()
@@ -29,10 +30,12 @@ test(`query: await cleanup, defaults`, async () => {
     })
   )
 
-  store.dispatch(api.endpoints.query.initiate('arg')).unsubscribe()
-  jest.advanceTimersByTime(59000), await waitMs()
+  const promise = store.dispatch(api.endpoints.query.initiate('arg'))
+  await promise
+  promise.unsubscribe()
+  vi.advanceTimersByTime(59000)
   expect(onCleanup).not.toHaveBeenCalled()
-  jest.advanceTimersByTime(2000), await waitMs()
+  vi.advanceTimersByTime(2000)
   expect(onCleanup).toHaveBeenCalled()
 })
 
@@ -49,10 +52,12 @@ test(`query: await cleanup, keepUnusedDataFor set`, async () => {
     })
   )
 
-  store.dispatch(api.endpoints.query.initiate('arg')).unsubscribe()
-  jest.advanceTimersByTime(28000), await waitMs()
+  const promise = store.dispatch(api.endpoints.query.initiate('arg'))
+  await promise
+  promise.unsubscribe()
+  vi.advanceTimersByTime(28000)
   expect(onCleanup).not.toHaveBeenCalled()
-  jest.advanceTimersByTime(2000), await waitMs()
+  vi.advanceTimersByTime(2000)
   expect(onCleanup).toHaveBeenCalled()
 })
 
@@ -69,20 +74,21 @@ test(`query: handles large keepUnuseDataFor values over 32-bit ms`, async () =>
     })
   )
 
-  store.dispatch(api.endpoints.query.initiate('arg')).unsubscribe()
+  const promise = store.dispatch(api.endpoints.query.initiate('arg'))
+  await promise
+  promise.unsubscribe()
 
   // Shouldn't have been called right away
-  jest.advanceTimersByTime(1000), await waitMs()
+  vi.advanceTimersByTime(1000)
   expect(onCleanup).not.toHaveBeenCalled()
 
   // Shouldn't have been called any time in the next few minutes
-  jest.advanceTimersByTime(1_000_000), await waitMs()
+  vi.advanceTimersByTime(1_000_000)
   expect(onCleanup).not.toHaveBeenCalled()
 
   // _Should_ be called _wayyyy_ in the future (like 24.8 days from now)
-  jest.advanceTimersByTime(THIRTY_TWO_BIT_MAX_TIMER_SECONDS * 1000),
-    await waitMs()
-  expect(onCleanup).toHaveBeenCalled()
+  vi.advanceTimersByTime(THIRTY_TWO_BIT_MAX_TIMER_SECONDS * 1000),
+    expect(onCleanup).toHaveBeenCalled()
 })
 
 describe(`query: await cleanup, keepUnusedDataFor set`, () => {
@@ -111,27 +117,33 @@ describe(`query: await cleanup, keepUnusedDataFor set`, () => {
   )
 
   test('global keepUnusedDataFor', async () => {
-    store.dispatch(api.endpoints.query.initiate('arg')).unsubscribe()
-    jest.advanceTimersByTime(28000), await waitMs()
+    const promise = store.dispatch(api.endpoints.query.initiate('arg'))
+    await promise
+    promise.unsubscribe()
+    vi.advanceTimersByTime(28000)
     expect(onCleanup).not.toHaveBeenCalled()
-    jest.advanceTimersByTime(2000), await waitMs()
+    vi.advanceTimersByTime(2000)
     expect(onCleanup).toHaveBeenCalled()
   })
 
   test('endpoint keepUnusedDataFor', async () => {
-    store.dispatch(api.endpoints.query2.initiate('arg')).unsubscribe()
-    jest.advanceTimersByTime(34000), await waitMs()
+    const promise = store.dispatch(api.endpoints.query2.initiate('arg'))
+    await promise
+    promise.unsubscribe()
+
+    vi.advanceTimersByTime(34000)
     expect(onCleanup).not.toHaveBeenCalled()
-    jest.advanceTimersByTime(2000), await waitMs()
+    vi.advanceTimersByTime(2000)
     expect(onCleanup).toHaveBeenCalled()
   })
 
   test('endpoint keepUnusedDataFor: 0 ', async () => {
     expect(onCleanup).not.toHaveBeenCalled()
-    store.dispatch(api.endpoints.query3.initiate('arg')).unsubscribe()
+    const promise = store.dispatch(api.endpoints.query3.initiate('arg'))
+    await promise
+    promise.unsubscribe()
     expect(onCleanup).not.toHaveBeenCalled()
-    jest.advanceTimersByTime(1)
-    await waitMs()
+    vi.advanceTimersByTime(1)
     expect(onCleanup).toHaveBeenCalled()
   })
 
@@ -139,7 +151,7 @@ describe(`query: await cleanup, keepUnusedDataFor set`, () => {
     expect(onCleanup).not.toHaveBeenCalled()
     store.dispatch(api.endpoints.query4.initiate('arg')).unsubscribe()
     expect(onCleanup).not.toHaveBeenCalled()
-    jest.advanceTimersByTime(THIRTY_TWO_BIT_MAX_INT)
+    vi.advanceTimersByTime(THIRTY_TWO_BIT_MAX_INT)
     expect(onCleanup).not.toHaveBeenCalled()
   })
 })
@@ -158,14 +170,18 @@ function storeForApi<
       gdm({ serializableCheck: false, immutableCheck: false }).concat(
         api.middleware
       ),
+    enhancers: (gde) =>
+      gde({
+        autoBatch: false,
+      }),
   })
   let hadQueries = false
   store.subscribe(() => {
     const queryState = store.getState().api.queries
-    if (hadQueries && Object.keys(queryState).length === 0) {
+    if (hadQueries && countObjectKeys(queryState) === 0) {
       onCleanup()
     }
-    hadQueries = hadQueries || Object.keys(queryState).length > 0
+    hadQueries = hadQueries || countObjectKeys(queryState) > 0
   })
   return { api, store }
 }
diff --git a/packages/toolkit/src/query/tests/cacheLifecycle.test.ts b/packages/toolkit/src/query/tests/cacheLifecycle.test.ts
--- a/packages/toolkit/src/query/tests/cacheLifecycle.test.ts
+++ b/packages/toolkit/src/query/tests/cacheLifecycle.test.ts
@@ -1,10 +1,17 @@
 import { createApi } from '@reduxjs/toolkit/query'
 import type { FetchBaseQueryMeta } from '@reduxjs/toolkit/query'
+import { vi } from 'vitest'
 import { fetchBaseQuery } from '@reduxjs/toolkit/query'
-import { expectType, fakeTimerWaitFor, setupApiStore, waitMs } from './helpers'
+import {
+  expectType,
+  fakeTimerWaitFor,
+  setupApiStore,
+  DEFAULT_DELAY_MS,
+} from './helpers'
+import { QueryActionCreatorResult } from '../core/buildInitiate'
 
 beforeAll(() => {
-  jest.useFakeTimers('legacy')
+  vi.useFakeTimers()
 })
 
 const api = createApi({
@@ -13,10 +20,10 @@ const api = createApi({
 })
 const storeRef = setupApiStore(api)
 
-const onNewCacheEntry = jest.fn()
-const gotFirstValue = jest.fn()
-const onCleanup = jest.fn()
-const onCatch = jest.fn()
+const onNewCacheEntry = vi.fn()
+const gotFirstValue = vi.fn()
+const onCleanup = vi.fn()
+const onCatch = vi.fn()
 
 beforeEach(() => {
   onNewCacheEntry.mockClear()
@@ -48,6 +55,7 @@ describe.each([['query'], ['mutation']] as const)(
       const extended = api.injectEndpoints({
         overrideExisting: true,
         endpoints: (build) => ({
+          // Lying to TS here
           injected: build[type as 'mutation']<unknown, string>({
             query: () => '/success',
             async onCacheEntryAdded(
@@ -68,11 +76,17 @@ describe.each([['query'], ['mutation']] as const)(
       expect(onNewCacheEntry).toHaveBeenCalledWith('arg')
       expect(onCleanup).not.toHaveBeenCalled()
 
-      promise.unsubscribe(), await waitMs()
+      await promise
+      if (type === 'mutation') {
+        promise.reset()
+      } else {
+        ;(promise as unknown as QueryActionCreatorResult<any>).unsubscribe()
+      }
+      await vi.advanceTimersByTimeAsync(DEFAULT_DELAY_MS)
       if (type === 'query') {
-        jest.advanceTimersByTime(59000), await waitMs()
+        await vi.advanceTimersByTimeAsync(59000)
         expect(onCleanup).not.toHaveBeenCalled()
-        jest.advanceTimersByTime(2000), await waitMs()
+        await vi.advanceTimersByTimeAsync(2000)
       }
 
       expect(onCleanup).toHaveBeenCalled()
@@ -121,11 +135,16 @@ describe.each([['query'], ['mutation']] as const)(
       })
       expect(onCleanup).not.toHaveBeenCalled()
 
-      promise.unsubscribe(), await waitMs()
+      if (type === 'mutation') {
+        promise.reset()
+      } else {
+        ;(promise as unknown as QueryActionCreatorResult<any>).unsubscribe()
+      }
+      await vi.advanceTimersByTimeAsync(DEFAULT_DELAY_MS)
       if (type === 'query') {
-        jest.advanceTimersByTime(59000), await waitMs()
+        await vi.advanceTimersByTimeAsync(59000)
         expect(onCleanup).not.toHaveBeenCalled()
-        jest.advanceTimersByTime(2000), await waitMs()
+        await vi.advanceTimersByTimeAsync(2000)
       }
 
       expect(onCleanup).toHaveBeenCalled()
@@ -158,9 +177,14 @@ describe.each([['query'], ['mutation']] as const)(
       )
       expect(onNewCacheEntry).toHaveBeenCalledWith('arg')
 
-      promise.unsubscribe(), await waitMs()
+      if (type === 'mutation') {
+        promise.reset()
+      } else {
+        ;(promise as unknown as QueryActionCreatorResult<any>).unsubscribe()
+      }
+      await vi.advanceTimersByTimeAsync(DEFAULT_DELAY_MS)
       if (type === 'query') {
-        jest.advanceTimersByTime(120000), await waitMs()
+        await vi.advanceTimersByTimeAsync(120000)
       }
       expect(gotFirstValue).not.toHaveBeenCalled()
       expect(onCleanup).not.toHaveBeenCalled()
@@ -196,11 +220,18 @@ describe.each([['query'], ['mutation']] as const)(
       )
 
       expect(onNewCacheEntry).toHaveBeenCalledWith('arg')
-      promise.unsubscribe(), await waitMs()
+      await promise
+      if (type === 'mutation') {
+        promise.reset()
+      } else {
+        ;(promise as unknown as QueryActionCreatorResult<any>).unsubscribe()
+      }
+      await vi.advanceTimersByTimeAsync(DEFAULT_DELAY_MS)
+
       if (type === 'query') {
-        jest.advanceTimersByTime(59000), await waitMs()
+        await vi.advanceTimersByTimeAsync(59000)
         expect(onCleanup).not.toHaveBeenCalled()
-        jest.advanceTimersByTime(2000), await waitMs()
+        await vi.advanceTimersByTimeAsync(2000)
       }
 
       expect(onCleanup).toHaveBeenCalled()
@@ -241,12 +272,18 @@ describe.each([['query'], ['mutation']] as const)(
       )
 
       expect(onNewCacheEntry).toHaveBeenCalledWith('arg')
+      await promise
 
-      promise.unsubscribe(), await waitMs()
+      if (type === 'mutation') {
+        promise.reset()
+      } else {
+        ;(promise as unknown as QueryActionCreatorResult<any>).unsubscribe()
+      }
+      await vi.advanceTimersByTimeAsync(DEFAULT_DELAY_MS)
       if (type === 'query') {
-        jest.advanceTimersByTime(59000), await waitMs()
+        await vi.advanceTimersByTimeAsync(59000)
         expect(onCleanup).not.toHaveBeenCalled()
-        jest.advanceTimersByTime(2000), await waitMs()
+        await vi.advanceTimersByTimeAsync(2000)
       }
       expect(onCleanup).not.toHaveBeenCalled()
       expect(gotFirstValue).not.toHaveBeenCalled()
@@ -287,11 +324,17 @@ describe.each([['query'], ['mutation']] as const)(
 
       expect(onNewCacheEntry).toHaveBeenCalledWith('arg')
 
-      promise.unsubscribe(), await waitMs()
+      await promise
+      if (type === 'mutation') {
+        promise.reset()
+      } else {
+        ;(promise as unknown as QueryActionCreatorResult<any>).unsubscribe()
+      }
+      await vi.advanceTimersByTimeAsync(DEFAULT_DELAY_MS)
       if (type === 'query') {
-        jest.advanceTimersByTime(59000), await waitMs()
+        await vi.advanceTimersByTimeAsync(59000)
         expect(onCleanup).not.toHaveBeenCalled()
-        jest.advanceTimersByTime(2000), await waitMs()
+        await vi.advanceTimersByTimeAsync(2000)
       }
       expect(onCleanup).toHaveBeenCalled()
       expect(gotFirstValue).not.toHaveBeenCalled()
@@ -303,7 +346,7 @@ describe.each([['query'], ['mutation']] as const)(
 )
 
 test(`query: getCacheEntry`, async () => {
-  const snapshot = jest.fn()
+  const snapshot = vi.fn()
   const extended = api.injectEndpoints({
     overrideExisting: true,
     endpoints: (build) => ({
@@ -331,13 +374,14 @@ test(`query: getCacheEntry`, async () => {
   const promise = storeRef.store.dispatch(
     extended.endpoints.injected.initiate('arg')
   )
+  await promise
   promise.unsubscribe()
 
   await fakeTimerWaitFor(() => {
     expect(gotFirstValue).toHaveBeenCalled()
   })
 
-  jest.advanceTimersByTime(120000), await waitMs()
+  await vi.advanceTimersByTimeAsync(120000)
 
   expect(snapshot).toHaveBeenCalledTimes(3)
   expect(snapshot.mock.calls[0][0]).toMatchObject({
@@ -376,7 +420,7 @@ test(`query: getCacheEntry`, async () => {
 })
 
 test(`mutation: getCacheEntry`, async () => {
-  const snapshot = jest.fn()
+  const snapshot = vi.fn()
   const extended = api.injectEndpoints({
     overrideExisting: true,
     endpoints: (build) => ({
@@ -408,7 +452,8 @@ test(`mutation: getCacheEntry`, async () => {
     expect(gotFirstValue).toHaveBeenCalled()
   })
 
-  promise.unsubscribe(), await waitMs()
+  promise.reset()
+  await vi.advanceTimersByTimeAsync(DEFAULT_DELAY_MS)
 
   expect(snapshot).toHaveBeenCalledTimes(3)
   expect(snapshot.mock.calls[0][0]).toMatchObject({
@@ -443,7 +488,7 @@ test(`mutation: getCacheEntry`, async () => {
 })
 
 test('updateCachedData', async () => {
-  const trackCalls = jest.fn()
+  const trackCalls = vi.fn()
 
   const extended = api.injectEndpoints({
     overrideExisting: true,
@@ -499,13 +544,14 @@ test('updateCachedData', async () => {
   const promise = storeRef.store.dispatch(
     extended.endpoints.injected.initiate('arg')
   )
+  await promise
   promise.unsubscribe()
 
   await fakeTimerWaitFor(() => {
     expect(gotFirstValue).toHaveBeenCalled()
   })
 
-  jest.advanceTimersByTime(61000)
+  await vi.advanceTimersByTimeAsync(61000)
 
   await fakeTimerWaitFor(() => {
     expect(onCleanup).toHaveBeenCalled()
@@ -536,7 +582,7 @@ test('dispatching further actions does not trigger another lifecycle', async ()
   expect(onNewCacheEntry).toHaveBeenCalledTimes(1)
 })
 
-test('dispatching a query initializer with `subscribe: false` does not start a lifecycle', async () => {
+test('dispatching a query initializer with `subscribe: false` does also start a lifecycle', async () => {
   const extended = api.injectEndpoints({
     overrideExisting: true,
     endpoints: (build) => ({
@@ -551,8 +597,9 @@ test('dispatching a query initializer with `subscribe: false` does not start a l
   await storeRef.store.dispatch(
     extended.endpoints.injected.initiate(undefined, { subscribe: false })
   )
-  expect(onNewCacheEntry).toHaveBeenCalledTimes(0)
+  expect(onNewCacheEntry).toHaveBeenCalledTimes(1)
 
+  // will not be called a second time though
   await storeRef.store.dispatch(extended.endpoints.injected.initiate(undefined))
   expect(onNewCacheEntry).toHaveBeenCalledTimes(1)
 })
diff --git a/packages/toolkit/src/query/tests/cleanup.test.tsx b/packages/toolkit/src/query/tests/cleanup.test.tsx
--- a/packages/toolkit/src/query/tests/cleanup.test.tsx
+++ b/packages/toolkit/src/query/tests/cleanup.test.tsx
@@ -1,16 +1,16 @@
 // tests for "cleanup-after-unsubscribe" behaviour
-
-import React, { Profiler, ProfilerOnRenderCallback } from 'react'
+import { vi } from 'vitest'
+import React from 'react'
 
 import { createListenerMiddleware } from '@reduxjs/toolkit'
 import { createApi, QueryStatus } from '@reduxjs/toolkit/query/react'
 import { render, waitFor, act, screen } from '@testing-library/react'
 import { setupApiStore } from './helpers'
-import { delay } from '../../utils'
+import { SubscriptionSelectors } from '../core/buildMiddleware/types'
 
 const tick = () => new Promise((res) => setImmediate(res))
 
-export const runAllTimers = async () => jest.runAllTimers() && (await tick())
+export const runAllTimers = async () => vi.runAllTimers() && (await tick())
 
 const api = createApi({
   baseQuery: () => ({ data: 42 }),
@@ -44,7 +44,7 @@ function UsingAB() {
 }
 
 beforeAll(() => {
-  jest.useFakeTimers('legacy')
+  vi.useFakeTimers()
 })
 
 test('data stays in store when component stays rendered', async () => {
@@ -55,11 +55,9 @@ test('data stays in store when component stays rendered', async () => {
     expect(getSubStateA()?.status).toBe(QueryStatus.fulfilled)
   )
 
-  jest.advanceTimersByTime(120000)
+  vi.advanceTimersByTime(120000)
 
-  await waitFor(() =>
-    expect(getSubStateA()?.status).toBe(QueryStatus.fulfilled)
-  )
+  expect(getSubStateA()?.status).toBe(QueryStatus.fulfilled)
 })
 
 test('data is removed from store after 60 seconds', async () => {
@@ -72,11 +70,11 @@ test('data is removed from store after 60 seconds', async () => {
 
   unmount()
 
-  jest.advanceTimersByTime(59000)
+  vi.advanceTimersByTime(59000)
 
   expect(getSubStateA()?.status).toBe(QueryStatus.fulfilled)
 
-  jest.advanceTimersByTime(2000)
+  vi.advanceTimersByTime(2000)
 
   expect(getSubStateA()).toBeUndefined()
 })
@@ -106,10 +104,10 @@ test('data stays in store when component stays rendered while data for another c
       </>
     )
 
-    jest.advanceTimersByTime(10)
+    vi.advanceTimersByTime(10)
   })
 
-  jest.advanceTimersByTime(120000)
+  vi.advanceTimersByTime(120000)
 
   expect(getSubStateA()).toEqual(statusA)
   expect(getSubStateB()).toBeUndefined()
@@ -140,13 +138,13 @@ test('data stays in store when one component requiring the data stays in the sto
         <UsingAB key="ab" />
       </>
     )
-    jest.advanceTimersByTime(10)
-    jest.runAllTimers()
+    vi.advanceTimersByTime(10)
+    vi.runAllTimers()
   })
 
   await act(async () => {
-    jest.advanceTimersByTime(120000)
-    jest.runAllTimers()
+    vi.advanceTimersByTime(120000)
+    vi.runAllTimers()
   })
 
   expect(getSubStateA()).toEqual(statusA)
@@ -162,18 +160,26 @@ test('Minimizes the number of subscription dispatches when multiple components a
     withoutTestLifecycles: true,
   })
 
-  let getSubscriptionsA = () =>
-    storeRef.store.getState().api.subscriptions['a(undefined)']
-
-  let actionTypes: string[] = []
+  let actionTypes: unknown[] = []
 
   listenerMiddleware.startListening({
     predicate: () => true,
     effect: (action) => {
+      if (
+        action.type.includes('subscriptionsUpdated') ||
+        action.type.includes('internal_')
+      ) {
+        return
+      }
+
       actionTypes.push(action.type)
     },
   })
 
+  const { getSubscriptionCount } = storeRef.store.dispatch(
+    api.internalActions.internal_getRTKQSubscriptions()
+  ) as unknown as SubscriptionSelectors
+
   const NUM_LIST_ITEMS = 1000
 
   function ParentComponent() {
@@ -188,22 +194,20 @@ test('Minimizes the number of subscription dispatches when multiple components a
     wrapper: storeRef.wrapper,
   })
 
-  jest.advanceTimersByTime(10)
+  await act(async () => {
+    vi.advanceTimersByTime(10)
+    vi.runAllTimers()
+  })
 
   await waitFor(() => {
     return screen.getAllByText(/42/).length > 0
   })
 
-  await runAllTimers()
-
-  const subscriptions = getSubscriptionsA()
-
-  expect(Object.keys(subscriptions!).length).toBe(NUM_LIST_ITEMS)
+  expect(getSubscriptionCount('a(undefined)')).toBe(NUM_LIST_ITEMS)
 
   expect(actionTypes).toEqual([
     'api/config/middlewareRegistered',
     'api/executeQuery/pending',
-    'api/internalSubscriptions/subscriptionsUpdated',
     'api/executeQuery/fulfilled',
   ])
 }, 25000)
diff --git a/packages/toolkit/src/query/tests/createApi.test.ts b/packages/toolkit/src/query/tests/createApi.test.ts
--- a/packages/toolkit/src/query/tests/createApi.test.ts
+++ b/packages/toolkit/src/query/tests/createApi.test.ts
@@ -1,5 +1,7 @@
-import { configureStore, createAction, createReducer } from '@reduxjs/toolkit'
 import type { SerializedError } from '@reduxjs/toolkit'
+import { configureStore, createAction, createReducer } from '@reduxjs/toolkit'
+import type { SpyInstance } from 'vitest'
+import { vi } from 'vitest'
 import type {
   Api,
   MutationDefinition,
@@ -33,9 +35,9 @@ const originalEnv = process.env.NODE_ENV
 beforeAll(() => void ((process.env as any).NODE_ENV = 'development'))
 afterAll(() => void ((process.env as any).NODE_ENV = originalEnv))
 
-let spy: jest.SpyInstance
+let spy: SpyInstance
 beforeAll(() => {
-  spy = jest.spyOn(console, 'error').mockImplementation(() => {})
+  spy = vi.spyOn(console, 'error').mockImplementation(() => {})
 })
 afterEach(() => {
   spy.mockReset()
@@ -84,7 +86,7 @@ test('sensible defaults', () => {
 })
 
 describe('wrong tagTypes log errors', () => {
-  const baseQuery = jest.fn()
+  const baseQuery = vi.fn()
   const api = createApi({
     baseQuery,
     tagTypes: ['User'],
@@ -328,7 +330,7 @@ describe('endpoint definition typings', () => {
   })
 
   describe('enhancing endpoint definitions', () => {
-    const baseQuery = jest.fn((x: string) => ({ data: 'success' }))
+    const baseQuery = vi.fn((x: string) => ({ data: 'success' }))
     const commonBaseQueryApi = {
       dispatch: expect.any(Function),
       endpoint: expect.any(String),
@@ -935,7 +937,7 @@ describe('custom serializeQueryArgs per endpoint', () => {
 
   type SuccessResponse = { value: 'success' }
 
-  const serializer1 = jest.fn(customArgsSerializer)
+  const serializer1 = vi.fn(customArgsSerializer)
 
   interface MyApiClient {
     fetchPost: (id: string) => Promise<SuccessResponse>
@@ -1042,7 +1044,7 @@ describe('custom serializeQueryArgs per endpoint', () => {
     ).toBeTruthy()
   })
 
-  const serializer2 = jest.fn(customArgsSerializer)
+  const serializer2 = vi.fn(customArgsSerializer)
 
   const injectedApi = api.injectEndpoints({
     endpoints: (build) => ({
diff --git a/packages/toolkit/src/query/tests/devWarnings.test.tsx b/packages/toolkit/src/query/tests/devWarnings.test.tsx
--- a/packages/toolkit/src/query/tests/devWarnings.test.tsx
+++ b/packages/toolkit/src/query/tests/devWarnings.test.tsx
@@ -6,7 +6,7 @@ import {
 } from 'console-testing-library/pure'
 import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
 
-let restore: () => void
+let restore: () => void = () => {}
 let nodeEnv: string
 
 beforeEach(() => {
@@ -205,7 +205,10 @@ describe('warns on multiple apis using the same `reducerPath`', () => {
   test('common: two apis, same order', async () => {
     const store = configureStore({
       reducer: {
+        // TS 5.3 now errors on identical object keys. We want to force that behavior.
+        // @ts-ignore
         [api1.reducerPath]: api1.reducer,
+        // @ts-ignore
         [api1_2.reducerPath]: api1_2.reducer,
       },
       middleware: (gDM) => gDM().concat(api1.middleware, api1_2.middleware),
@@ -222,7 +225,9 @@ If you have multiple apis, you *have* to specify the reducerPath option when usi
   test('common: two apis, opposing order', async () => {
     const store = configureStore({
       reducer: {
+        // @ts-ignore
         [api1.reducerPath]: api1.reducer,
+        // @ts-ignore
         [api1_2.reducerPath]: api1_2.reducer,
       },
       middleware: (gDM) => gDM().concat(api1_2.middleware, api1.middleware),
@@ -242,7 +247,9 @@ If you have multiple apis, you *have* to specify the reducerPath option when usi
   test('common: two apis, only first middleware', async () => {
     const store = configureStore({
       reducer: {
+        // @ts-ignore
         [api1.reducerPath]: api1.reducer,
+        // @ts-ignore
         [api1_2.reducerPath]: api1_2.reducer,
       },
       middleware: (gDM) => gDM().concat(api1.middleware),
@@ -266,7 +273,9 @@ If you have multiple apis, you *have* to specify the reducerPath option when usi
   test.skip('common: two apis, only second middleware', async () => {
     const store = configureStore({
       reducer: {
+        // @ts-ignore
         [api1.reducerPath]: api1.reducer,
+        // @ts-ignore
         [api1_2.reducerPath]: api1_2.reducer,
       },
       middleware: (gDM) => gDM().concat(api1_2.middleware),
diff --git a/packages/toolkit/src/query/tests/errorHandling.test.tsx b/packages/toolkit/src/query/tests/errorHandling.test.tsx
--- a/packages/toolkit/src/query/tests/errorHandling.test.tsx
+++ b/packages/toolkit/src/query/tests/errorHandling.test.tsx
@@ -6,9 +6,16 @@ import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'
 import axios from 'axios'
 import { expectExactType, hookWaitFor, setupApiStore } from './helpers'
 import { server } from './mocks/server'
-import { fireEvent, render, waitFor, screen, act, renderHook } from '@testing-library/react'
+import {
+  fireEvent,
+  render,
+  waitFor,
+  screen,
+  act,
+  renderHook,
+} from '@testing-library/react'
 import { useDispatch } from 'react-redux'
-import type { AnyAction, ThunkDispatch } from '@reduxjs/toolkit'
+import type { UnknownAction, ThunkDispatch } from '@reduxjs/toolkit'
 import type { BaseQueryApi } from '../baseQueryTypes'
 
 const baseQuery = fetchBaseQuery({ baseUrl: 'https://example.com' })
@@ -549,7 +556,11 @@ describe('error handling in a component', () => {
     test(`an un-subscribed mutation will still return something useful (success case, track: ${track})`, async () => {
       const hook = renderHook(useDispatch, { wrapper: storeRef.wrapper })
 
-      const dispatch = hook.result.current as ThunkDispatch<any, any, AnyAction>
+      const dispatch = hook.result.current as ThunkDispatch<
+        any,
+        any,
+        UnknownAction
+      >
       let mutationqueryFulfilled: ReturnType<
         ReturnType<typeof api.endpoints.update.initiate>
       >
@@ -567,7 +578,11 @@ describe('error handling in a component', () => {
     test(`an un-subscribed mutation will still return something useful (error case, track: ${track})`, async () => {
       const hook = renderHook(useDispatch, { wrapper: storeRef.wrapper })
 
-      const dispatch = hook.result.current as ThunkDispatch<any, any, AnyAction>
+      const dispatch = hook.result.current as ThunkDispatch<
+        any,
+        any,
+        UnknownAction
+      >
       let mutationqueryFulfilled: ReturnType<
         ReturnType<typeof api.endpoints.failedUpdate.initiate>
       >
@@ -587,7 +602,11 @@ describe('error handling in a component', () => {
     test(`an un-subscribed mutation will still be unwrappable (success case), track: ${track}`, async () => {
       const hook = renderHook(useDispatch, { wrapper: storeRef.wrapper })
 
-      const dispatch = hook.result.current as ThunkDispatch<any, any, AnyAction>
+      const dispatch = hook.result.current as ThunkDispatch<
+        any,
+        any,
+        UnknownAction
+      >
       let mutationqueryFulfilled: ReturnType<
         ReturnType<typeof api.endpoints.update.initiate>
       >
@@ -605,7 +624,11 @@ describe('error handling in a component', () => {
     test(`an un-subscribed mutation will still be unwrappable (error case, track: ${track})`, async () => {
       const hook = renderHook(useDispatch, { wrapper: storeRef.wrapper })
 
-      const dispatch = hook.result.current as ThunkDispatch<any, any, AnyAction>
+      const dispatch = hook.result.current as ThunkDispatch<
+        any,
+        any,
+        UnknownAction
+      >
       let mutationqueryFulfilled: ReturnType<
         ReturnType<typeof api.endpoints.failedUpdate.initiate>
       >
diff --git a/packages/toolkit/src/query/tests/fetchBaseQuery.test.tsx b/packages/toolkit/src/query/tests/fetchBaseQuery.test.tsx
--- a/packages/toolkit/src/query/tests/fetchBaseQuery.test.tsx
+++ b/packages/toolkit/src/query/tests/fetchBaseQuery.test.tsx
@@ -1,3 +1,4 @@
+import { vi } from 'vitest'
 import { createSlice } from '@reduxjs/toolkit'
 import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
 import { setupApiStore, waitMs } from './helpers'
@@ -18,7 +19,7 @@ const defaultHeaders: Record<string, string> = {
 const baseUrl = 'https://example.com'
 
 // @ts-ignore
-const fetchFn = jest.fn<Promise<any>, any[]>(global.fetch)
+const fetchFn = vi.fn<Promise<any>, any[]>(global.fetch)
 
 const baseQuery = fetchBaseQuery({
   baseUrl,
@@ -175,7 +176,7 @@ describe('fetchBaseQuery', () => {
       expect(res.meta?.response).toBeInstanceOf(Object)
       expect(res.error).toEqual({
         status: 'PARSING_ERROR',
-        error: 'SyntaxError: Unexpected token h in JSON at position 1',
+        error: expect.stringMatching(/SyntaxError: Unexpected token/),
         originalStatus: 200,
         data: `this is not json!`,
       })
@@ -333,7 +334,7 @@ describe('fetchBaseQuery', () => {
       expect(res.meta?.response).toBeInstanceOf(Object)
       expect(res.error).toEqual({
         status: 'PARSING_ERROR',
-        error: 'SyntaxError: Unexpected token h in JSON at position 1',
+        error: expect.stringMatching(/SyntaxError: Unexpected token/),
         originalStatus: 500,
         data: `this is not json!`,
       })
@@ -434,7 +435,7 @@ describe('fetchBaseQuery', () => {
 
     it('supports a custom jsonReplacer', async () => {
       const body = {
-        items: new Set(["A", "B", "C"])
+        items: new Set(['A', 'B', 'C']),
       }
 
       let request: any
@@ -455,7 +456,8 @@ describe('fetchBaseQuery', () => {
       const baseQueryWithReplacer = fetchBaseQuery({
         baseUrl,
         fetchFn: fetchFn as any,
-        jsonReplacer: (key, value) => value instanceof Set ? [...value] : value
+        jsonReplacer: (key, value) =>
+          value instanceof Set ? [...value] : value,
       })
 
       ;({ data: request } = await baseQueryWithReplacer(
@@ -469,8 +471,7 @@ describe('fetchBaseQuery', () => {
       ))
 
       expect(request.headers['content-type']).toBe('application/json')
-      expect(request.body).toEqual({ items: ["A", "B", "C"] }) // Set is marshalled correctly by jsonReplacer
-      
+      expect(request.body).toEqual({ items: ['A', 'B', 'C'] }) // Set is marshalled correctly by jsonReplacer
     })
   })
 
@@ -1025,7 +1026,7 @@ describe('fetchFn', () => {
       clone: () => fakeResponse,
     }
 
-    const spiedFetch = jest.spyOn(window, 'fetch')
+    const spiedFetch = vi.spyOn(window, 'fetch')
     spiedFetch.mockResolvedValueOnce(fakeResponse as any)
 
     const { data } = await baseQuery({ url: '/echo' }, commonBaseQueryApi, {})
diff --git a/packages/toolkit/src/query/tests/helpers.tsx b/packages/toolkit/src/query/tests/helpers.tsx
--- a/packages/toolkit/src/query/tests/helpers.tsx
+++ b/packages/toolkit/src/query/tests/helpers.tsx
@@ -1,13 +1,14 @@
+import React, { useCallback } from 'react'
 import type {
-  AnyAction,
+  UnknownAction,
   EnhancedStore,
   Middleware,
   Store,
+  Reducer,
 } from '@reduxjs/toolkit'
 import { configureStore } from '@reduxjs/toolkit'
 import { setupListeners } from '@reduxjs/toolkit/query'
-import type { Reducer } from 'react'
-import React, { useCallback } from 'react'
+
 import { Provider } from 'react-redux'
 
 import {
@@ -57,11 +58,29 @@ export const hookWaitFor = async (cb: () => void, time = 2000) => {
       if (Date.now() > startedAt + time) {
         throw e
       }
-      await act(() => waitMs(2))
+      await act(async () => {
+        await waitMs(2)
+      })
+    }
+  }
+}
+export const fakeTimerWaitFor = async (cb: () => void, time = 2000) => {
+  const startedAt = Date.now()
+
+  while (true) {
+    try {
+      cb()
+      return true
+    } catch (e) {
+      if (Date.now() > startedAt + time) {
+        throw e
+      }
+      await act(async () => {
+        await vi.advanceTimersByTimeAsync(2)
+      })
     }
   }
 }
-export const fakeTimerWaitFor = hookWaitFor
 
 export const useRenderCounter = () => {
   const countRef = React.useRef(0)
@@ -89,7 +108,7 @@ declare global {
 
 expect.extend({
   toMatchSequence(
-    _actions: AnyAction[],
+    _actions: UnknownAction[],
     ...matchers: Array<(arg: any) => boolean>
   ) {
     const actions = _actions.concat()
@@ -99,7 +118,9 @@ expect.extend({
       if (!matchers[i](actions[i])) {
         return {
           message: () =>
-            `Action ${actions[i].type} does not match sequence at position ${i}.`,
+            `Action ${actions[i].type} does not match sequence at position ${i}.
+All actions:
+${actions.map((a) => a.type).join('\n')}`,
           pass: false,
         }
       }
@@ -160,7 +181,12 @@ ${expectedOutput}
 })
 
 export const actionsReducer = {
-  actions: (state: AnyAction[] = [], action: AnyAction) => {
+  actions: (state: UnknownAction[] = [], action: UnknownAction) => {
+    // As of 2.0-beta.4, we are going to ignore all `subscriptionsUpdated` actions in tests
+    if (action.type.includes('subscriptionsUpdated')) {
+      return state
+    }
+
     return [...state, action]
   },
 }
@@ -196,18 +222,27 @@ export function setupApiStore<
         }).concat(api.middleware)
 
         return tempMiddleware
-          .concat(...(middleware?.concat ?? []))
-          .prepend(...(middleware?.prepend ?? [])) as typeof tempMiddleware
+          .concat(middleware?.concat ?? [])
+          .prepend(middleware?.prepend ?? []) as typeof tempMiddleware
       },
+      enhancers: (gde) =>
+        gde({
+          autoBatch: false,
+        }),
     })
 
+  type State = {
+    api: ReturnType<A['reducer']>
+  } & {
+    [K in keyof R]: ReturnType<R[K]>
+  }
   type StoreType = EnhancedStore<
     {
       api: ReturnType<A['reducer']>
     } & {
       [K in keyof R]: ReturnType<R[K]>
     },
-    AnyAction,
+    UnknownAction,
     ReturnType<typeof getStore> extends EnhancedStore<any, any, infer M>
       ? M
       : never
diff --git a/packages/toolkit/src/query/tests/matchers.test.tsx b/packages/toolkit/src/query/tests/matchers.test.tsx
--- a/packages/toolkit/src/query/tests/matchers.test.tsx
+++ b/packages/toolkit/src/query/tests/matchers.test.tsx
@@ -65,25 +65,22 @@ test('matches query pending & fulfilled actions for the given endpoint', async (
   expect(storeRef.store.getState().actions).toMatchSequence(
     api.internalActions.middlewareRegistered.match,
     endpoint.matchPending,
-    api.internalActions.subscriptionsUpdated.match,
     endpoint.matchFulfilled
   )
   expect(storeRef.store.getState().actions).not.toMatchSequence(
     api.internalActions.middlewareRegistered.match,
     otherEndpoint.matchPending,
-    api.internalActions.subscriptionsUpdated.match,
     otherEndpoint.matchFulfilled
   )
   expect(storeRef.store.getState().actions).not.toMatchSequence(
     api.internalActions.middlewareRegistered.match,
     endpoint.matchFulfilled,
-    api.internalActions.subscriptionsUpdated.match,
+    api.endpoints.mutationSuccess.matchFulfilled,
     endpoint.matchRejected
   )
   expect(storeRef.store.getState().actions).not.toMatchSequence(
     api.internalActions.middlewareRegistered.match,
     endpoint.matchPending,
-    api.internalActions.subscriptionsUpdated.match,
     endpoint.matchRejected
   )
 })
@@ -96,7 +93,6 @@ test('matches query pending & rejected actions for the given endpoint', async ()
   expect(storeRef.store.getState().actions).toMatchSequence(
     api.internalActions.middlewareRegistered.match,
     endpoint.matchPending,
-    api.internalActions.subscriptionsUpdated.match,
     endpoint.matchRejected
   )
   expect(storeRef.store.getState().actions).not.toMatchSequence(
@@ -122,20 +118,17 @@ test('matches lazy query pending & fulfilled actions for given endpoint', async
   expect(storeRef.store.getState().actions).toMatchSequence(
     api.internalActions.middlewareRegistered.match,
     endpoint.matchPending,
-    api.internalActions.subscriptionsUpdated.match,
     endpoint.matchFulfilled
   )
   expect(storeRef.store.getState().actions).not.toMatchSequence(
     api.internalActions.middlewareRegistered.match,
     endpoint.matchFulfilled,
-    api.internalActions.subscriptionsUpdated.match,
     endpoint.matchRejected
   )
 
   expect(storeRef.store.getState().actions).not.toMatchSequence(
     api.internalActions.middlewareRegistered.match,
     endpoint.matchPending,
-    api.internalActions.subscriptionsUpdated.match,
     endpoint.matchRejected
   )
 })
@@ -151,7 +144,6 @@ test('matches lazy query pending & rejected actions for given endpoint', async (
   expect(storeRef.store.getState().actions).toMatchSequence(
     api.internalActions.middlewareRegistered.match,
     endpoint.matchPending,
-    api.internalActions.subscriptionsUpdated.match,
     endpoint.matchRejected
   )
   expect(storeRef.store.getState().actions).not.toMatchSequence(
diff --git a/packages/toolkit/src/query/tests/optimisticUpdates.test.tsx b/packages/toolkit/src/query/tests/optimisticUpdates.test.tsx
--- a/packages/toolkit/src/query/tests/optimisticUpdates.test.tsx
+++ b/packages/toolkit/src/query/tests/optimisticUpdates.test.tsx
@@ -1,3 +1,4 @@
+import { vi } from 'vitest'
 import { createApi } from '@reduxjs/toolkit/query/react'
 import { actionsReducer, hookWaitFor, setupApiStore, waitMs } from './helpers'
 import { renderHook, act } from '@testing-library/react'
@@ -9,7 +10,7 @@ interface Post {
   contents: string
 }
 
-const baseQuery = jest.fn()
+const baseQuery = vi.fn()
 beforeEach(() => baseQuery.mockReset())
 
 const api = createApi({
@@ -58,9 +59,9 @@ const storeRef = setupApiStore(api, {
 })
 
 describe('basic lifecycle', () => {
-  let onStart = jest.fn(),
-    onError = jest.fn(),
-    onSuccess = jest.fn()
+  let onStart = vi.fn(),
+    onError = vi.fn(),
+    onSuccess = vi.fn()
 
   const extendedApi = api.injectEndpoints({
     endpoints: (build) => ({
@@ -117,14 +118,13 @@ describe('basic lifecycle', () => {
       }
     )
 
-    baseQuery.mockRejectedValue('error')
-
+    baseQuery.mockRejectedValueOnce('error')
     expect(onStart).not.toHaveBeenCalled()
     expect(baseQuery).not.toHaveBeenCalled()
+
     act(() => void result.current[0]('arg'))
     expect(onStart).toHaveBeenCalledWith('arg')
     expect(baseQuery).toHaveBeenCalledWith('arg', expect.any(Object), undefined)
-
     expect(onError).not.toHaveBeenCalled()
     expect(onSuccess).not.toHaveBeenCalled()
     await act(() => waitMs(5))
diff --git a/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx b/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx
--- a/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx
+++ b/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx
@@ -1,3 +1,4 @@
+import { vi } from 'vitest'
 import { createApi } from '@reduxjs/toolkit/query/react'
 import { actionsReducer, hookWaitFor, setupApiStore, waitMs } from './helpers'
 import { skipToken } from '../core/buildSelectors'
@@ -10,7 +11,7 @@ interface Post {
   contents: string
 }
 
-const baseQuery = jest.fn()
+const baseQuery = vi.fn()
 beforeEach(() => baseQuery.mockReset())
 
 const api = createApi({
@@ -67,9 +68,9 @@ const storeRef = setupApiStore(api, {
 })
 
 describe('basic lifecycle', () => {
-  let onStart = jest.fn(),
-    onError = jest.fn(),
-    onSuccess = jest.fn()
+  let onStart = vi.fn(),
+    onError = vi.fn(),
+    onSuccess = vi.fn()
 
   const extendedApi = api.injectEndpoints({
     endpoints: (build) => ({
@@ -161,7 +162,7 @@ describe('basic lifecycle', () => {
       }
     )
 
-    baseQuery.mockRejectedValue('error')
+    baseQuery.mockRejectedValueOnce('error')
 
     expect(onStart).not.toHaveBeenCalled()
     expect(baseQuery).not.toHaveBeenCalled()
@@ -295,7 +296,7 @@ describe('upsertQueryData', () => {
     expect(state.data).toEqual(fetchedData)
   })
   test('upsert while a normal query is running (rejected)', async () => {
-    baseQuery.mockImplementation(async () => {
+    baseQuery.mockImplementationOnce(async () => {
       await delay(20)
       // eslint-disable-next-line no-throw-literal
       throw 'Error!'
diff --git a/packages/toolkit/src/query/tests/polling.test.tsx b/packages/toolkit/src/query/tests/polling.test.tsx
--- a/packages/toolkit/src/query/tests/polling.test.tsx
+++ b/packages/toolkit/src/query/tests/polling.test.tsx
@@ -1,8 +1,10 @@
+import { vi } from 'vitest'
 import { createApi } from '@reduxjs/toolkit/query'
 import { setupApiStore, waitMs } from './helpers'
 import { delay } from '../../utils'
+import type { SubscriptionSelectors } from '../core/buildMiddleware/types'
 
-const mockBaseQuery = jest
+const mockBaseQuery = vi
   .fn()
   .mockImplementation((args: any) => ({ data: args }))
 
@@ -22,8 +24,16 @@ const { getPosts } = api.endpoints
 
 const storeRef = setupApiStore(api)
 
+let getSubscriptions: SubscriptionSelectors['getSubscriptions']
+
+beforeEach(() => {
+  ;({ getSubscriptions } = storeRef.store.dispatch(
+    api.internalActions.internal_getRTKQSubscriptions()
+  ) as unknown as SubscriptionSelectors)
+})
+
 const getSubscribersForQueryCacheKey = (queryCacheKey: string) =>
-  storeRef.store.getState()[api.reducerPath].subscriptions[queryCacheKey] || {}
+  getSubscriptions()[queryCacheKey] || {}
 const createSubscriptionGetter = (queryCacheKey: string) => () =>
   getSubscribersForQueryCacheKey(queryCacheKey)
 
diff --git a/packages/toolkit/src/query/tests/queryFn.test.tsx b/packages/toolkit/src/query/tests/queryFn.test.tsx
--- a/packages/toolkit/src/query/tests/queryFn.test.tsx
+++ b/packages/toolkit/src/query/tests/queryFn.test.tsx
@@ -1,3 +1,4 @@
+import { vi } from 'vitest'
 import type { SerializedError } from '@reduxjs/toolkit'
 import { configureStore } from '@reduxjs/toolkit'
 import type { BaseQueryFn, FetchBaseQueryError } from '@reduxjs/toolkit/query'
@@ -9,9 +10,11 @@ import type { QuerySubState } from '@reduxjs/toolkit/dist/query/core/apiState'
 
 describe('queryFn base implementation tests', () => {
   const baseQuery: BaseQueryFn<string, { wrappedByBaseQuery: string }, string> =
-    jest.fn((arg: string) => arg.includes('withErrorQuery')
-      ? ({ error: `cut${arg}` })
-      : ({ data: { wrappedByBaseQuery: arg } }))
+    vi.fn((arg: string) =>
+      arg.includes('withErrorQuery')
+        ? { error: `cut${arg}` }
+        : { data: { wrappedByBaseQuery: arg } }
+    )
 
   const api = createApi({
     baseQuery,
@@ -308,11 +311,11 @@ describe('usage scenario tests', () => {
     exists: () => true,
     data: () => mockData,
   }
-  const get = jest.fn(() => Promise.resolve(mockDocResult))
-  const doc = jest.fn((name) => ({
+  const get = vi.fn(() => Promise.resolve(mockDocResult))
+  const doc = vi.fn((name) => ({
     get,
   }))
-  const collection = jest.fn((name) => ({ get, doc }))
+  const collection = vi.fn((name) => ({ get, doc }))
   const firestore = () => {
     return { collection, doc }
   }
diff --git a/packages/toolkit/src/query/tests/queryLifecycle.test.tsx b/packages/toolkit/src/query/tests/queryLifecycle.test.tsx
--- a/packages/toolkit/src/query/tests/queryLifecycle.test.tsx
+++ b/packages/toolkit/src/query/tests/queryLifecycle.test.tsx
@@ -1,3 +1,4 @@
+import { vi } from 'vitest'
 import { createApi } from '@reduxjs/toolkit/query'
 import { waitFor } from '@testing-library/react'
 import type {
@@ -15,9 +16,9 @@ const api = createApi({
 })
 const storeRef = setupApiStore(api)
 
-const onStart = jest.fn()
-const onSuccess = jest.fn()
-const onError = jest.fn()
+const onStart = vi.fn()
+const onSuccess = vi.fn()
+const onError = vi.fn()
 
 beforeEach(() => {
   onStart.mockClear()
@@ -113,7 +114,7 @@ describe.each([['query'], ['mutation']] as const)(
 )
 
 test('query: getCacheEntry (success)', async () => {
-  const snapshot = jest.fn()
+  const snapshot = vi.fn()
   const extended = api.injectEndpoints({
     overrideExisting: true,
     endpoints: (build) => ({
@@ -174,7 +175,7 @@ test('query: getCacheEntry (success)', async () => {
 })
 
 test('query: getCacheEntry (error)', async () => {
-  const snapshot = jest.fn()
+  const snapshot = vi.fn()
   const extended = api.injectEndpoints({
     overrideExisting: true,
     endpoints: (build) => ({
@@ -234,7 +235,7 @@ test('query: getCacheEntry (error)', async () => {
 })
 
 test('mutation: getCacheEntry (success)', async () => {
-  const snapshot = jest.fn()
+  const snapshot = vi.fn()
   const extended = api.injectEndpoints({
     overrideExisting: true,
     endpoints: (build) => ({
@@ -291,7 +292,7 @@ test('mutation: getCacheEntry (success)', async () => {
 })
 
 test('mutation: getCacheEntry (error)', async () => {
-  const snapshot = jest.fn()
+  const snapshot = vi.fn()
   const extended = api.injectEndpoints({
     overrideExisting: true,
     endpoints: (build) => ({
@@ -347,7 +348,7 @@ test('mutation: getCacheEntry (error)', async () => {
 })
 
 test('query: updateCachedData', async () => {
-  const trackCalls = jest.fn()
+  const trackCalls = vi.fn()
 
   const extended = api.injectEndpoints({
     overrideExisting: true,
diff --git a/packages/toolkit/src/query/tests/raceConditions.test.ts b/packages/toolkit/src/query/tests/raceConditions.test.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/query/tests/raceConditions.test.ts
@@ -0,0 +1,109 @@
+import { createApi, QueryStatus } from '@reduxjs/toolkit/query'
+import { getLog } from 'console-testing-library'
+import { actionsReducer, setupApiStore, waitMs } from './helpers'
+
+// We need to be able to control when which query resolves to simulate race
+// conditions properly, that's the purpose of this factory.
+const createPromiseFactory = () => {
+  const resolveQueue: (() => void)[] = []
+  const createPromise = () =>
+    new Promise<void>((resolve) => {
+      resolveQueue.push(resolve)
+    })
+  const resolveOldest = () => {
+    resolveQueue.shift()?.()
+  }
+  return { createPromise, resolveOldest }
+}
+
+const getEatenBananaPromises = createPromiseFactory()
+const eatBananaPromises = createPromiseFactory()
+
+let eatenBananas = 0
+const api = createApi({
+  invalidationBehavior: 'delayed',
+  baseQuery: () => undefined as any,
+  tagTypes: ['Banana'],
+  endpoints: (build) => ({
+    // Eat a banana.
+    eatBanana: build.mutation<unknown, void>({
+      queryFn: async () => {
+        await eatBananaPromises.createPromise()
+        eatenBananas += 1
+        return { data: null, meta: {} }
+      },
+      invalidatesTags: ['Banana'],
+    }),
+
+    // Get the number of eaten bananas.
+    getEatenBananas: build.query<number, void>({
+      queryFn: async (arg, arg1, arg2, arg3) => {
+        const result = eatenBananas
+        await getEatenBananaPromises.createPromise()
+        return { data: result }
+      },
+      providesTags: ['Banana'],
+    }),
+  }),
+})
+const { getEatenBananas, eatBanana } = api.endpoints
+
+const storeRef = setupApiStore(api, {
+  ...actionsReducer,
+})
+
+it('invalidates a query after a corresponding mutation', async () => {
+  eatenBananas = 0
+
+  const query = storeRef.store.dispatch(getEatenBananas.initiate())
+  const getQueryState = () =>
+    storeRef.store.getState().api.queries[query.queryCacheKey]
+  getEatenBananaPromises.resolveOldest()
+  await waitMs(2)
+
+  expect(getQueryState()?.data).toBe(0)
+  expect(getQueryState()?.status).toBe(QueryStatus.fulfilled)
+
+  const mutation = storeRef.store.dispatch(eatBanana.initiate())
+  const getMutationState = () =>
+    storeRef.store.getState().api.mutations[mutation.requestId]
+  eatBananaPromises.resolveOldest()
+  await waitMs(2)
+
+  expect(getMutationState()?.status).toBe(QueryStatus.fulfilled)
+  expect(getQueryState()?.data).toBe(0)
+  expect(getQueryState()?.status).toBe(QueryStatus.pending)
+
+  getEatenBananaPromises.resolveOldest()
+  await waitMs(2)
+
+  expect(getQueryState()?.data).toBe(1)
+  expect(getQueryState()?.status).toBe(QueryStatus.fulfilled)
+})
+
+it('invalidates a query whose corresponding mutation finished while the query was in flight', async () => {
+  eatenBananas = 0
+
+  const query = storeRef.store.dispatch(getEatenBananas.initiate())
+  const getQueryState = () =>
+    storeRef.store.getState().api.queries[query.queryCacheKey]
+
+  const mutation = storeRef.store.dispatch(eatBanana.initiate())
+  const getMutationState = () =>
+    storeRef.store.getState().api.mutations[mutation.requestId]
+  eatBananaPromises.resolveOldest()
+  await waitMs(2)
+  expect(getMutationState()?.status).toBe(QueryStatus.fulfilled)
+
+  getEatenBananaPromises.resolveOldest()
+  await waitMs(2)
+  expect(getQueryState()?.data).toBe(0)
+  expect(getQueryState()?.status).toBe(QueryStatus.pending)
+
+  // should already be refetching
+  getEatenBananaPromises.resolveOldest()
+  await waitMs(2)
+
+  expect(getQueryState()?.status).toBe(QueryStatus.fulfilled)
+  expect(getQueryState()?.data).toBe(1)
+})
diff --git a/packages/toolkit/src/query/tests/refetchingBehaviors.test.tsx b/packages/toolkit/src/query/tests/refetchingBehaviors.test.tsx
--- a/packages/toolkit/src/query/tests/refetchingBehaviors.test.tsx
+++ b/packages/toolkit/src/query/tests/refetchingBehaviors.test.tsx
@@ -1,3 +1,4 @@
+import { vi } from 'vitest'
 import * as React from 'react'
 import { createApi, setupListeners } from '@reduxjs/toolkit/query/react'
 import { act, fireEvent, render, waitFor, screen } from '@testing-library/react'
@@ -372,11 +373,11 @@ describe('customListenersHandler', () => {
   })
 
   test('setupListeners accepts a custom callback and executes it', async () => {
-    const consoleSpy = jest.spyOn(console, 'log')
-    consoleSpy.mockImplementation((...args) => {
+    const consoleSpy = vi.spyOn(console, 'log')
+    consoleSpy.mockImplementation((...args: any[]) => {
       // console.info(...args)
     })
-    const dispatchSpy = jest.spyOn(storeRef.store, 'dispatch')
+    const dispatchSpy = vi.spyOn(storeRef.store, 'dispatch')
 
     let unsubscribe = () => {}
     unsubscribe = setupListeners(
@@ -431,10 +432,12 @@ describe('customListenersHandler', () => {
     })
     expect(dispatchSpy).toHaveBeenCalled()
 
-    // Ignore RTKQ middleware `internal_probeSubscription` calls
-    const mockCallsWithoutInternals = dispatchSpy.mock.calls.filter(
-      (call) => !(call[0] as any)?.type?.includes('internal')
-    )
+    // Ignore RTKQ middleware internal data calls
+    const mockCallsWithoutInternals = dispatchSpy.mock.calls.filter((call) => {
+      const type = (call[0] as any)?.type ?? ''
+      const reIsInternal = /internal/i
+      return !reIsInternal.test(type)
+    })
 
     expect(
       defaultApi.internalActions.onOnline.match(
diff --git a/packages/toolkit/src/query/tests/retry.test.ts b/packages/toolkit/src/query/tests/retry.test.ts
--- a/packages/toolkit/src/query/tests/retry.test.ts
+++ b/packages/toolkit/src/query/tests/retry.test.ts
@@ -1,26 +1,29 @@
+import { vi } from 'vitest'
 import type { BaseQueryFn } from '@reduxjs/toolkit/query'
 import { createApi, retry } from '@reduxjs/toolkit/query'
 import { setupApiStore, waitMs } from './helpers'
 import type { RetryOptions } from '../retry'
 
 beforeEach(() => {
-  jest.useFakeTimers('legacy')
+  vi.useFakeTimers()
 })
 
 const loopTimers = async (max: number = 12) => {
   let count = 0
   while (count < max) {
-    await waitMs(1)
-    jest.advanceTimersByTime(120000)
+    await vi.advanceTimersByTimeAsync(1)
+    vi.advanceTimersByTime(120000)
     count++
   }
 }
 
+vi.fn()
+
 describe('configuration', () => {
   test('retrying without any config options', async () => {
-    const baseBaseQuery = jest.fn<
-      ReturnType<BaseQueryFn>,
-      Parameters<BaseQueryFn>
+    const baseBaseQuery = vi.fn<
+      Parameters<BaseQueryFn>,
+      ReturnType<BaseQueryFn>
     >()
     baseBaseQuery.mockResolvedValue({ error: 'rejected' })
 
@@ -45,9 +48,9 @@ describe('configuration', () => {
   })
 
   test('retrying with baseQuery config that overrides default behavior (maxRetries: 5)', async () => {
-    const baseBaseQuery = jest.fn<
-      ReturnType<BaseQueryFn>,
-      Parameters<BaseQueryFn>
+    const baseBaseQuery = vi.fn<
+      Parameters<BaseQueryFn>,
+      ReturnType<BaseQueryFn>
     >()
     baseBaseQuery.mockResolvedValue({ error: 'rejected' })
 
@@ -72,9 +75,9 @@ describe('configuration', () => {
   })
 
   test('retrying with endpoint config that overrides baseQuery config', async () => {
-    const baseBaseQuery = jest.fn<
-      ReturnType<BaseQueryFn>,
-      Parameters<BaseQueryFn>
+    const baseBaseQuery = vi.fn<
+      Parameters<BaseQueryFn>,
+      ReturnType<BaseQueryFn>
     >()
     baseBaseQuery.mockResolvedValue({ error: 'rejected' })
 
@@ -111,9 +114,9 @@ describe('configuration', () => {
   })
 
   test('stops retrying a query after a success', async () => {
-    const baseBaseQuery = jest.fn<
-      ReturnType<BaseQueryFn>,
-      Parameters<BaseQueryFn>
+    const baseBaseQuery = vi.fn<
+      Parameters<BaseQueryFn>,
+      ReturnType<BaseQueryFn>
     >()
     baseBaseQuery
       .mockResolvedValueOnce({ error: 'rejected' })
@@ -141,9 +144,9 @@ describe('configuration', () => {
   })
 
   test('retrying also works with mutations', async () => {
-    const baseBaseQuery = jest.fn<
-      ReturnType<BaseQueryFn>,
-      Parameters<BaseQueryFn>
+    const baseBaseQuery = vi.fn<
+      Parameters<BaseQueryFn>,
+      ReturnType<BaseQueryFn>
     >()
     baseBaseQuery.mockResolvedValue({ error: 'rejected' })
 
@@ -169,9 +172,9 @@ describe('configuration', () => {
   })
 
   test('retrying stops after a success from a mutation', async () => {
-    const baseBaseQuery = jest.fn<
-      ReturnType<BaseQueryFn>,
-      Parameters<BaseQueryFn>
+    const baseBaseQuery = vi.fn<
+      Parameters<BaseQueryFn>,
+      ReturnType<BaseQueryFn>
     >()
     baseBaseQuery
       .mockRejectedValueOnce(new Error('rejected'))
@@ -199,9 +202,9 @@ describe('configuration', () => {
     expect(baseBaseQuery).toHaveBeenCalledTimes(3)
   })
   test('non-error-cases should **not** retry', async () => {
-    const baseBaseQuery = jest.fn<
-      ReturnType<BaseQueryFn>,
-      Parameters<BaseQueryFn>
+    const baseBaseQuery = vi.fn<
+      Parameters<BaseQueryFn>,
+      ReturnType<BaseQueryFn>
     >()
     baseBaseQuery.mockResolvedValue({ data: { success: true } })
 
@@ -228,9 +231,9 @@ describe('configuration', () => {
   test('calling retry.fail(error) will skip retrying and expose the error directly', async () => {
     const error = { message: 'banana' }
 
-    const baseBaseQuery = jest.fn<
-      ReturnType<BaseQueryFn>,
-      Parameters<BaseQueryFn>
+    const baseBaseQuery = vi.fn<
+      Parameters<BaseQueryFn>,
+      ReturnType<BaseQueryFn>
     >()
     baseBaseQuery.mockImplementation((input) => {
       retry.fail(error)
@@ -276,9 +279,9 @@ describe('configuration', () => {
      * Note:
      * This will retry 16 total times because we try the initial + 3 retries (sum: 4), then retry that process 3 times (starting at 0 for a total of 4)... 4x4=16 (allegedly)
      */
-    const baseBaseQuery = jest.fn<
-      ReturnType<BaseQueryFn>,
-      Parameters<BaseQueryFn>
+    const baseBaseQuery = vi.fn<
+      Parameters<BaseQueryFn>,
+      ReturnType<BaseQueryFn>
     >()
     baseBaseQuery.mockResolvedValue({ error: 'rejected' })
 
@@ -306,9 +309,9 @@ describe('configuration', () => {
   })
 
   test('accepts a custom backoff fn', async () => {
-    const baseBaseQuery = jest.fn<
-      ReturnType<BaseQueryFn>,
-      Parameters<BaseQueryFn>
+    const baseBaseQuery = vi.fn<
+      Parameters<BaseQueryFn>,
+      ReturnType<BaseQueryFn>
     >()
     baseBaseQuery.mockResolvedValue({ error: 'rejected' })
 
@@ -342,9 +345,9 @@ describe('configuration', () => {
   })
 
   test('accepts a custom retryCondition fn', async () => {
-    const baseBaseQuery = jest.fn<
-      ReturnType<BaseQueryFn>,
-      Parameters<BaseQueryFn>
+    const baseBaseQuery = vi.fn<
+      Parameters<BaseQueryFn>,
+      ReturnType<BaseQueryFn>
     >()
     baseBaseQuery.mockResolvedValue({ error: 'rejected' })
 
@@ -373,9 +376,9 @@ describe('configuration', () => {
   })
 
   test('retryCondition with endpoint config that overrides baseQuery config', async () => {
-    const baseBaseQuery = jest.fn<
-      ReturnType<BaseQueryFn>,
-      Parameters<BaseQueryFn>
+    const baseBaseQuery = vi.fn<
+      Parameters<BaseQueryFn>,
+      ReturnType<BaseQueryFn>
     >()
     baseBaseQuery.mockResolvedValue({ error: 'rejected' })
 
@@ -405,9 +408,9 @@ describe('configuration', () => {
   })
 
   test('retryCondition also works with mutations', async () => {
-    const baseBaseQuery = jest.fn<
-      ReturnType<BaseQueryFn>,
-      Parameters<BaseQueryFn>
+    const baseBaseQuery = vi.fn<
+      Parameters<BaseQueryFn>,
+      ReturnType<BaseQueryFn>
     >()
 
     baseBaseQuery
@@ -440,9 +443,9 @@ describe('configuration', () => {
   })
 
   test('Specifying maxRetries as 0 in RetryOptions prevents retries', async () => {
-    const baseBaseQuery = jest.fn<
-      ReturnType<BaseQueryFn>,
-      Parameters<BaseQueryFn>
+    const baseBaseQuery = vi.fn<
+      Parameters<BaseQueryFn>,
+      ReturnType<BaseQueryFn>
     >()
     baseBaseQuery.mockResolvedValue({ error: 'rejected' })
 
@@ -464,7 +467,7 @@ describe('configuration', () => {
     await loopTimers(2)
 
     expect(baseBaseQuery).toHaveBeenCalledTimes(1)
-  });
+  })
 
   test.skip('RetryOptions only accepts one of maxRetries or retryCondition', () => {
     // @ts-expect-error Should complain if both exist at once
diff --git a/packages/toolkit/src/query/tests/utils.test.ts b/packages/toolkit/src/query/tests/utils.test.ts
--- a/packages/toolkit/src/query/tests/utils.test.ts
+++ b/packages/toolkit/src/query/tests/utils.test.ts
@@ -1,3 +1,4 @@
+import { vi } from 'vitest'
 import {
   isOnline,
   isDocumentVisible,
@@ -6,68 +7,68 @@ import {
 } from '@internal/query/utils'
 
 afterAll(() => {
-  jest.restoreAllMocks()
+  vi.restoreAllMocks()
 })
 
 describe('isOnline', () => {
   test('Assumes online=true in a node env', () => {
-    jest
-      .spyOn(window, 'navigator', 'get')
-      .mockImplementation(() => undefined as any)
+    vi.spyOn(window, 'navigator', 'get').mockImplementation(
+      () => undefined as any
+    )
 
     expect(navigator).toBeUndefined()
     expect(isOnline()).toBe(true)
   })
 
   test('Returns false if navigator isOnline=false', () => {
-    jest
-      .spyOn(window, 'navigator', 'get')
-      .mockImplementation(() => ({ onLine: false } as any))
+    vi.spyOn(window, 'navigator', 'get').mockImplementation(
+      () => ({ onLine: false } as any)
+    )
     expect(isOnline()).toBe(false)
   })
 
   test('Returns true if navigator isOnline=true', () => {
-    jest
-      .spyOn(window, 'navigator', 'get')
-      .mockImplementation(() => ({ onLine: true } as any))
+    vi.spyOn(window, 'navigator', 'get').mockImplementation(
+      () => ({ onLine: true } as any)
+    )
     expect(isOnline()).toBe(true)
   })
 })
 
 describe('isDocumentVisible', () => {
   test('Assumes true when in a non-browser env', () => {
-    jest
-      .spyOn(window, 'document', 'get')
-      .mockImplementation(() => undefined as any)
+    vi.spyOn(window, 'document', 'get').mockImplementation(
+      () => undefined as any
+    )
     expect(window.document).toBeUndefined()
     expect(isDocumentVisible()).toBe(true)
   })
 
   test('Returns false when hidden=true', () => {
-    jest
-      .spyOn(window, 'document', 'get')
-      .mockImplementation(() => ({ visibilityState: 'hidden' } as any))
+    vi.spyOn(window, 'document', 'get').mockImplementation(
+      () => ({ visibilityState: 'hidden' } as any)
+    )
     expect(isDocumentVisible()).toBe(false)
   })
 
   test('Returns true when visibilityState=prerender', () => {
-    jest
-      .spyOn(window, 'document', 'get')
-      .mockImplementation(() => ({ visibilityState: 'prerender' } as any))
+    vi.spyOn(window, 'document', 'get').mockImplementation(
+      () => ({ visibilityState: 'prerender' } as any)
+    )
     expect(document.visibilityState).toBe('prerender')
     expect(isDocumentVisible()).toBe(true)
   })
   test('Returns true when visibilityState=visible', () => {
-    jest
-      .spyOn(window, 'document', 'get')
-      .mockImplementation(() => ({ visibilityState: 'visible' } as any))
+    vi.spyOn(window, 'document', 'get').mockImplementation(
+      () => ({ visibilityState: 'visible' } as any)
+    )
     expect(document.visibilityState).toBe('visible')
     expect(isDocumentVisible()).toBe(true)
   })
   test('Returns true when visibilityState=undefined', () => {
-    jest
-      .spyOn(window, 'document', 'get')
-      .mockImplementation(() => ({ visibilityState: undefined } as any))
+    vi.spyOn(window, 'document', 'get').mockImplementation(
+      () => ({ visibilityState: undefined } as any)
+    )
     expect(document.visibilityState).toBeUndefined()
     expect(isDocumentVisible()).toBe(true)
   })
diff --git a/packages/toolkit/src/tests/Tuple.typetest.ts b/packages/toolkit/src/tests/Tuple.typetest.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/tests/Tuple.typetest.ts
@@ -0,0 +1,81 @@
+import { Tuple } from '@reduxjs/toolkit'
+import { expectType } from './helpers'
+
+/**
+ * Test: compatibility is checked between described types
+ */
+{
+  const stringTuple = new Tuple('')
+
+  expectType<Tuple<[string]>>(stringTuple)
+
+  expectType<Tuple<string[]>>(stringTuple)
+
+  // @ts-expect-error
+  expectType<Tuple<[string, string]>>(stringTuple)
+
+  const numberTuple = new Tuple(0, 1)
+  // @ts-expect-error
+  expectType<Tuple<string[]>>(numberTuple)
+}
+
+/**
+ * Test: concat is inferred properly
+ */
+{
+  const singleString = new Tuple('')
+
+  expectType<Tuple<[string]>>(singleString)
+
+  expectType<Tuple<[string, string]>>(singleString.concat(''))
+
+  expectType<Tuple<[string, string]>>(singleString.concat(['']))
+}
+
+/**
+ * Test: prepend is inferred properly
+ */
+{
+  const singleString = new Tuple('')
+
+  expectType<Tuple<[string]>>(singleString)
+
+  expectType<Tuple<[string, string]>>(singleString.prepend(''))
+
+  expectType<Tuple<[string, string]>>(singleString.prepend(['']))
+}
+
+/**
+ * Test: push must match existing items
+ */
+{
+  const stringTuple = new Tuple('')
+
+  stringTuple.push('')
+
+  // @ts-expect-error
+  stringTuple.push(0)
+}
+
+/**
+ * Test: Tuples can be combined
+ */
+{
+  const stringTuple = new Tuple('')
+
+  const numberTuple = new Tuple(0, 1)
+
+  expectType<Tuple<[string, number, number]>>(stringTuple.concat(numberTuple))
+
+  expectType<Tuple<[number, number, string]>>(stringTuple.prepend(numberTuple))
+
+  expectType<Tuple<[number, number, string]>>(numberTuple.concat(stringTuple))
+
+  expectType<Tuple<[string, number, number]>>(numberTuple.prepend(stringTuple))
+
+  // @ts-expect-error
+  expectType<Tuple<[string, number, number]>>(stringTuple.prepend(numberTuple))
+
+  // @ts-expect-error
+  expectType<Tuple<[number, number, string]>>(stringTuple.concat(numberTuple))
+}
diff --git a/packages/toolkit/src/tests/__snapshots__/createAsyncThunk.test.ts.snap b/packages/toolkit/src/tests/__snapshots__/createAsyncThunk.test.ts.snap
deleted file mode 100644
--- a/packages/toolkit/src/tests/__snapshots__/createAsyncThunk.test.ts.snap
+++ /dev/null
@@ -1,8 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`createAsyncThunk with abortController behaviour with missing AbortController calling \`abort\` on an asyncThunk works with a FallbackAbortController if no global abortController is not available 1`] = `
-"This platform does not implement AbortController. 
-If you want to use the AbortController to react to \`abort\` events, please consider importing a polyfill like 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'."
-`;
-
-exports[`non-serializable arguments are ignored by serializableStateInvariantMiddleware 1`] = `""`;
diff --git a/packages/toolkit/src/tests/__snapshots__/serializableStateInvariantMiddleware.test.ts.snap b/packages/toolkit/src/tests/__snapshots__/serializableStateInvariantMiddleware.test.ts.snap
deleted file mode 100644
--- a/packages/toolkit/src/tests/__snapshots__/serializableStateInvariantMiddleware.test.ts.snap
+++ /dev/null
@@ -1,54 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`serializableStateInvariantMiddleware Should log an error when a non-serializable action is dispatched 1`] = `
-"A non-serializable value was detected in an action, in the path: \`type\`. Value: Symbol(SOME_CONSTANT) 
-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) 
-(To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)"
-`;
-
-exports[`serializableStateInvariantMiddleware Should log an error when a non-serializable value is in state 1`] = `
-"A non-serializable value was detected in the state, in the path: \`testSlice.a\`. Value: Map {} 
-Take a look at the reducer(s) handling this action type: TEST_ACTION.
-(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
-`;
-
-exports[`serializableStateInvariantMiddleware allows ignoring state entirely 1`] = `""`;
-
-exports[`serializableStateInvariantMiddleware consumer tolerated structures Should log an error when a non-serializable value is nested in state 1`] = `
-"A non-serializable value was detected in the state, in the path: \`testSlice.a.entries\`. Value: [Function entries] 
-Take a look at the reducer(s) handling this action type: TEST_ACTION.
-(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
-`;
-
-exports[`serializableStateInvariantMiddleware consumer tolerated structures Should use consumer supplied isSerializable and getEntries options to tolerate certain structures 1`] = `
-"A non-serializable value was detected in the state, in the path: \`testSlice.a.third.bad-map-instance\`. Value: Map {} 
-Take a look at the reducer(s) handling this action type: TEST_ACTION.
-(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
-`;
-
-exports[`serializableStateInvariantMiddleware ignored action paths can specify (multiple) different values 1`] = `""`;
-
-exports[`serializableStateInvariantMiddleware ignored action paths can specify regexp 1`] = `""`;
-
-exports[`serializableStateInvariantMiddleware ignored action paths default value can be overridden 1`] = `
-"A non-serializable value was detected in an action, in the path: \`meta.arg\`. Value: Map {} 
-Take a look at the logic that dispatched this action:  Object {
-  \\"meta\\": Object {
-    \\"arg\\": Map {},
-  },
-  \\"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) 
-(To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)"
-`;
-
-exports[`serializableStateInvariantMiddleware ignored action paths default value: meta.arg 1`] = `""`;
-
-exports[`serializableStateInvariantMiddleware should not check serializability for ignored slice names 1`] = `
-"A non-serializable value was detected in the state, in the path: \`testSlice.b.d\`. Value: Map {} 
-Take a look at the reducer(s) handling this action type: TEST_ACTION.
-(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
-`;
diff --git a/packages/toolkit/src/tests/actionCreatorInvariantMiddleware.test.ts b/packages/toolkit/src/tests/actionCreatorInvariantMiddleware.test.ts
--- a/packages/toolkit/src/tests/actionCreatorInvariantMiddleware.test.ts
+++ b/packages/toolkit/src/tests/actionCreatorInvariantMiddleware.test.ts
@@ -1,11 +1,11 @@
 import type { ActionCreatorInvariantMiddlewareOptions } from '@internal/actionCreatorInvariantMiddleware'
 import { getMessage } from '@internal/actionCreatorInvariantMiddleware'
 import { createActionCreatorInvariantMiddleware } from '@internal/actionCreatorInvariantMiddleware'
-import type { Dispatch, MiddlewareAPI } from '@reduxjs/toolkit'
+import type { MiddlewareAPI } from '@reduxjs/toolkit'
 import { createAction } from '@reduxjs/toolkit'
 
 describe('createActionCreatorInvariantMiddleware', () => {
-  const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
+  const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
 
   afterEach(() => {
     consoleSpy.mockClear()
@@ -17,17 +17,14 @@ describe('createActionCreatorInvariantMiddleware', () => {
   const dummyAction = createAction('aSlice/anAction')
 
   it('sends the action through the middleware chain', () => {
-    const next: Dispatch = (action) => ({
-      ...action,
-      returned: true,
-    })
+    const next = vi.fn()
     const dispatch = createActionCreatorInvariantMiddleware()(
       {} as MiddlewareAPI
     )(next)
+    dispatch({ type: 'SOME_ACTION' })
 
-    expect(dispatch(dummyAction())).toEqual({
-      ...dummyAction(),
-      returned: true,
+    expect(next).toHaveBeenCalledWith({
+      type: 'SOME_ACTION',
     })
   })
 
diff --git a/packages/toolkit/src/tests/autoBatchEnhancer.test.ts b/packages/toolkit/src/tests/autoBatchEnhancer.test.ts
--- a/packages/toolkit/src/tests/autoBatchEnhancer.test.ts
+++ b/packages/toolkit/src/tests/autoBatchEnhancer.test.ts
@@ -31,9 +31,10 @@ const { incrementBatched, decrementUnbatched } = counterSlice.actions
 const makeStore = (autoBatchOptions?: AutoBatchOptions) => {
   return configureStore({
     reducer: counterSlice.reducer,
-    enhancers: (existingEnhancers) => {
-      return existingEnhancers.concat(autoBatchEnhancer(autoBatchOptions))
-    },
+    enhancers: (getDefaultEnhancers) =>
+      getDefaultEnhancers({
+        autoBatch: autoBatchOptions,
+      }),
   })
 }
 
diff --git a/packages/toolkit/src/tests/combineSlices.test.ts b/packages/toolkit/src/tests/combineSlices.test.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/tests/combineSlices.test.ts
@@ -0,0 +1,175 @@
+import { createReducer } from '../createReducer'
+import { createAction } from '../createAction'
+import { createSlice } from '../createSlice'
+import type { WithSlice } from '../combineSlices'
+import { combineSlices } from '../combineSlices'
+import { expectType } from './helpers'
+import type { CombinedState } from '../query/core/apiState'
+
+const dummyAction = createAction<void>('dummy')
+
+const stringSlice = createSlice({
+  name: 'string',
+  initialState: '',
+  reducers: {},
+})
+
+const numberSlice = createSlice({
+  name: 'number',
+  initialState: 0,
+  reducers: {},
+})
+
+const booleanReducer = createReducer(false, () => {})
+
+// mimic - we can't use RTKQ here directly
+const api = {
+  reducerPath: 'api' as const,
+  reducer: createReducer(
+    expectType<CombinedState<{}, never, 'api'>>({
+      queries: {},
+      mutations: {},
+      provided: {},
+      subscriptions: {},
+      config: {
+        reducerPath: 'api',
+        invalidationBehavior: 'delayed',
+        online: false,
+        focused: false,
+        keepUnusedDataFor: 60,
+        middlewareRegistered: false,
+        refetchOnMountOrArgChange: false,
+        refetchOnReconnect: false,
+        refetchOnFocus: false,
+      },
+    }),
+    () => {}
+  ),
+}
+
+describe('combineSlices', () => {
+  it('calls combineReducers to combine static slices/reducers', () => {
+    const combinedReducer = combineSlices(
+      stringSlice,
+      {
+        num: numberSlice.reducer,
+        boolean: booleanReducer,
+      },
+      api
+    )
+    expect(combinedReducer(undefined, dummyAction())).toEqual({
+      string: stringSlice.getInitialState(),
+      num: numberSlice.getInitialState(),
+      boolean: booleanReducer.getInitialState(),
+      api: api.reducer.getInitialState(),
+    })
+  })
+  describe('injects', () => {
+    it('injects slice', () => {
+      const combinedReducer =
+        combineSlices(stringSlice).withLazyLoadedSlices<
+          WithSlice<typeof numberSlice>
+        >()
+
+      expect(combinedReducer(undefined, dummyAction()).number).toBe(undefined)
+
+      const injectedReducer = combinedReducer.inject(numberSlice)
+
+      expect(injectedReducer(undefined, dummyAction()).number).toBe(
+        numberSlice.getInitialState()
+      )
+    })
+    it('logs error when same name is used for different reducers', () => {
+      const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
+      const combinedReducer =
+        combineSlices(stringSlice).withLazyLoadedSlices<{ boolean: boolean }>()
+
+      combinedReducer.inject({
+        reducerPath: 'boolean' as const,
+        reducer: booleanReducer,
+      })
+
+      combinedReducer.inject({
+        reducerPath: 'boolean' as const,
+        reducer: booleanReducer,
+      })
+
+      expect(consoleSpy).not.toHaveBeenCalled()
+
+      combinedReducer.inject({
+        reducerPath: 'boolean' as const,
+        // @ts-expect-error wrong reducer
+        reducer: stringSlice.reducer,
+      })
+
+      expect(consoleSpy).toHaveBeenCalledWith(
+        `called \`inject\` to override already-existing reducer boolean without specifying \`overrideExisting: true\``
+      )
+      consoleSpy.mockRestore()
+    })
+    it('allows replacement of reducers if overrideExisting is true', () => {
+      const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
+      const combinedReducer = combineSlices(stringSlice).withLazyLoadedSlices<
+        WithSlice<typeof numberSlice> &
+          WithSlice<typeof api> & { boolean: boolean }
+      >()
+
+      combinedReducer.inject(numberSlice)
+
+      combinedReducer.inject(
+        { reducerPath: 'number' as const, reducer: () => 0 },
+        { overrideExisting: true }
+      )
+
+      expect(consoleSpy).not.toHaveBeenCalled()
+    })
+  })
+  describe('selector', () => {
+    const combinedReducer =
+      combineSlices(stringSlice).withLazyLoadedSlices<{ boolean: boolean }>()
+
+    const uninjectedState = combinedReducer(undefined, dummyAction())
+
+    const injectedReducer = combinedReducer.inject({
+      reducerPath: 'boolean' as const,
+      reducer: booleanReducer,
+    })
+
+    it('ensures state is defined in selector even if action has not been dispatched', () => {
+      expect(uninjectedState.boolean).toBe(undefined)
+
+      const selectBoolean = injectedReducer.selector((state) => state.boolean)
+
+      expect(selectBoolean(uninjectedState)).toBe(
+        booleanReducer.getInitialState()
+      )
+    })
+    it('exposes original to allow for logging', () => {
+      const selectBoolean = injectedReducer.selector(
+        (state) => injectedReducer.selector.original(state).boolean
+      )
+      expect(selectBoolean(uninjectedState)).toBe(undefined)
+    })
+    it('throws if original is called on something other than state proxy', () => {
+      expect(() => injectedReducer.selector.original({} as any)).toThrow(
+        'original must be used on state Proxy'
+      )
+    })
+    it('allows passing a selectState selector, to handle nested state', () => {
+      const wrappedReducer = combineSlices({
+        inner: combinedReducer,
+      })
+
+      type RootState = ReturnType<typeof wrappedReducer>
+
+      const selector = injectedReducer.selector(
+        (state) => state.boolean,
+        (rootState: RootState) => rootState.inner
+      )
+
+      expect(selector(wrappedReducer(undefined, dummyAction()))).toBe(
+        booleanReducer.getInitialState()
+      )
+    })
+  })
+})
diff --git a/packages/toolkit/src/tests/combineSlices.typetest.ts b/packages/toolkit/src/tests/combineSlices.typetest.ts
new file mode 100644
--- /dev/null
+++ b/packages/toolkit/src/tests/combineSlices.typetest.ts
@@ -0,0 +1,210 @@
+/* eslint-disable no-lone-blocks */
+import type { Reducer, Slice, WithSlice } from '@reduxjs/toolkit'
+import { combineSlices } from '@reduxjs/toolkit'
+import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
+import { expectExactType, expectType } from './helpers'
+
+declare const stringSlice: Slice<string, {}, 'string'>
+
+declare const numberSlice: Slice<number, {}, 'number'>
+
+declare const booleanReducer: Reducer<boolean>
+
+const exampleApi = createApi({
+  baseQuery: fetchBaseQuery(),
+  endpoints: (build) => ({
+    getThing: build.query({
+      query: () => '',
+    }),
+  }),
+})
+
+type ExampleApiState = ReturnType<typeof exampleApi.reducer>
+
+/**
+ * Test: combineSlices correctly combines static state
+ */
+{
+  const rootReducer = combineSlices(stringSlice, numberSlice, exampleApi, {
+    boolean: booleanReducer,
+  })
+  expectType<{
+    string: string
+    number: number
+    boolean: boolean
+    api: ExampleApiState
+  }>(rootReducer(undefined, { type: '' }))
+}
+
+/**
+ * Test: withLazyLoadedSlices adds partial to state
+ */
+{
+  const rootReducer = combineSlices(stringSlice).withLazyLoadedSlices<
+    WithSlice<typeof numberSlice> & WithSlice<typeof exampleApi>
+  >()
+  expectExactType<number | undefined>(0)(
+    rootReducer(undefined, { type: '' }).number
+  )
+  expectExactType<ExampleApiState | undefined>(undefined)(
+    rootReducer(undefined, { type: '' }).api
+  )
+}
+
+/**
+ * Test: inject marks injected keys as required
+ */
+{
+  const rootReducer = combineSlices(stringSlice).withLazyLoadedSlices<
+    WithSlice<typeof numberSlice> &
+      WithSlice<typeof exampleApi> & { boolean: boolean }
+  >()
+
+  expectExactType<number | undefined>(0)(
+    rootReducer(undefined, { type: '' }).number
+  )
+  expectExactType<boolean | undefined>(true)(
+    rootReducer(undefined, { type: '' }).boolean
+  )
+  expectExactType<ExampleApiState | undefined>(undefined)(
+    rootReducer(undefined, { type: '' }).api
+  )
+
+  const withNumber = rootReducer.inject(numberSlice)
+  expectExactType<number>(0)(withNumber(undefined, { type: '' }).number)
+
+  const withBool = rootReducer.inject({
+    reducerPath: 'boolean' as const,
+    reducer: booleanReducer,
+  })
+  expectExactType<boolean>(true)(withBool(undefined, { type: '' }).boolean)
+
+  const withApi = rootReducer.inject(exampleApi)
+  expectExactType<ExampleApiState>({} as ExampleApiState)(
+    withApi(undefined, { type: '' }).api
+  )
+}
+
+declare const wrongNumberSlice: Slice<string, {}, 'number'>
+
+declare const wrongBooleanReducer: Reducer<number>
+
+const wrongApi = createApi({
+  baseQuery: fetchBaseQuery(),
+  endpoints: (build) => ({
+    getThing2: build.query({
+      query: () => '',
+    }),
+  }),
+})
+
+/**
+ * Test: selector() allows defining selectors with injected reducers defined
+ */
+{
+  const rootReducer = combineSlices(stringSlice).withLazyLoadedSlices<
+    WithSlice<typeof numberSlice> & { boolean: boolean }
+  >()
+
+  type RootState = ReturnType<typeof rootReducer>
+
+  const withoutInjection = rootReducer.selector(
+    (state: RootState) => state.number
+  )
+
+  expectExactType<number | undefined>(0)(
+    withoutInjection(rootReducer(undefined, { type: '' }))
+  )
+
+  const withInjection = rootReducer
+    .inject(numberSlice)
+    .selector((state) => state.number)
+
+  expectExactType<number>(0)(
+    withInjection(rootReducer(undefined, { type: '' }))
+  )
+}
+
+/**
+ * Test: selector() passes arguments through
+ */
+{
+  const rootReducer = combineSlices(stringSlice).withLazyLoadedSlices<
+    WithSlice<typeof numberSlice> & { boolean: boolean }
+  >()
+
+  const selector = rootReducer
+    .inject(numberSlice)
+    .selector((state, num: number) => state.number)
+
+  const state = rootReducer(undefined, { type: '' })
+  // @ts-expect-error required argument
+  selector(state)
+  // @ts-expect-error number not string
+  selector(state, '')
+  selector(state, 0)
+}
+
+/**
+ * Test: nested calls inferred correctly
+ */
+{
+  const innerReducer =
+    combineSlices(stringSlice).withLazyLoadedSlices<
+      WithSlice<typeof numberSlice>
+    >()
+
+  const innerSelector = innerReducer.inject(numberSlice).selector(
+    (state) => state.number,
+    (rootState: RootState) => rootState.inner
+  )
+
+  const outerReducer = combineSlices({ inner: innerReducer })
+
+  type RootState = ReturnType<typeof outerReducer>
+
+  expectType<{ inner: { string: string } }>(
+    outerReducer(undefined, { type: '' })
+  )
+
+  expectType<number>(innerSelector(outerReducer(undefined, { type: '' })))
+}
+
+/**
+ * Test: selector errors if selectorFn and selectState are mismatched
+ */
+
+{
+  const combinedReducer =
+    combineSlices(stringSlice).withLazyLoadedSlices<
+      WithSlice<typeof numberSlice>
+    >()
+
+  const outerReducer = combineSlices({ inner: combinedReducer })
+
+  type RootState = ReturnType<typeof outerReducer>
+
+  combinedReducer.selector(
+    (state) => state.number,
+    // @ts-expect-error wrong state returned
+    (rootState: RootState) => rootState.inner.number
+  )
+  combinedReducer.selector(
+    (state, num: number) => state.number,
+    // @ts-expect-error wrong arguments
+    (rootState: RootState, str: string) => rootState.inner
+  )
+
+  combinedReducer.selector(
+    (state, num: number) => state.number,
+    (rootState: RootState) => rootState.inner
+  )
+
+  // TODO: see if there's a way of making this work
+  // probably a rare case so not the end of the world if not
+  combinedReducer.selector(
+    (state) => state.number,
+    // @ts-ignore
+    (rootState: RootState, num: number) => rootState.inner
+  )
+}
diff --git a/packages/toolkit/src/tests/combinedTest.test.ts b/packages/toolkit/src/tests/combinedTest.test.ts
--- a/packages/toolkit/src/tests/combinedTest.test.ts
+++ b/packages/toolkit/src/tests/combinedTest.test.ts
@@ -10,7 +10,7 @@ import type { EntityAdapter } from '@internal/entities/models'
 import type { BookModel } from '@internal/entities/tests/fixtures/book'
 
 describe('Combined entity slice', () => {
-  let adapter: EntityAdapter<BookModel>
+  let adapter: EntityAdapter<BookModel, string>
 
   beforeEach(() => {
     adapter = createEntityAdapter({
diff --git a/packages/toolkit/src/tests/configureStore.test.ts b/packages/toolkit/src/tests/configureStore.test.ts
--- a/packages/toolkit/src/tests/configureStore.test.ts
+++ b/packages/toolkit/src/tests/configureStore.test.ts
@@ -1,31 +1,82 @@
-import type { StoreEnhancer, StoreEnhancerStoreCreator } from '@reduxjs/toolkit'
-import { configureStore } from '@reduxjs/toolkit'
-import * as RTK from '@reduxjs/toolkit'
-import * as redux from 'redux'
-import * as devtools from '@internal/devtoolsExtension'
+import { vi } from 'vitest'
+import type { StoreEnhancer } from '@reduxjs/toolkit'
+import { Tuple } from '@reduxjs/toolkit'
+import type * as Redux from 'redux'
+import type * as DevTools from '@internal/devtoolsExtension'
 
-describe('configureStore', () => {
-  jest.spyOn(redux, 'applyMiddleware')
-  jest.spyOn(redux, 'combineReducers')
-  jest.spyOn(redux, 'compose')
-  jest.spyOn(redux, 'createStore')
-  jest.spyOn(devtools, 'composeWithDevTools') // @remap-prod-remove-line
+vi.doMock('redux', async () => {
+  const redux: any = await vi.importActual('redux')
 
-  const reducer: redux.Reducer = (state = {}, _action) => state
+  vi.spyOn(redux, 'applyMiddleware')
+  vi.spyOn(redux, 'combineReducers')
+  vi.spyOn(redux, 'compose')
+  vi.spyOn(redux, 'createStore')
 
-  beforeEach(() => jest.clearAllMocks())
+  return redux
+})
+
+vi.doMock('@internal/devtoolsExtension', async () => {
+  const devtools: typeof DevTools = await vi.importActual(
+    '@internal/devtoolsExtension'
+  )
+  vi.spyOn(devtools, 'composeWithDevTools') // @remap-prod-remove-line
+  return devtools
+})
+
+function originalReduxCompose(...funcs: Function[]) {
+  if (funcs.length === 0) {
+    // infer the argument type so it is usable in inference down the line
+    return <T>(arg: T) => arg
+  }
+
+  if (funcs.length === 1) {
+    return funcs[0]
+  }
+
+  return funcs.reduce(
+    (a, b) =>
+      (...args: any) =>
+        a(b(...args))
+  )
+}
+
+function originalComposeWithDevtools() {
+  if (arguments.length === 0) return undefined
+  if (typeof arguments[0] === 'object') return originalReduxCompose
+  return originalReduxCompose.apply(null, arguments as any as Function[])
+}
+
+describe('configureStore', async () => {
+  // RTK's internal `composeWithDevtools` function isn't publicly exported,
+  // so we can't mock it. However, it _does_ try to access the global extension method
+  // attached to `window`. So, if we mock _that_, we'll know if the enhancer ran.
+  const mockDevtoolsCompose = vi
+    .fn()
+    .mockImplementation(originalComposeWithDevtools)
+  ;(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = mockDevtoolsCompose
+
+  const redux = await import('redux')
+
+  const { configureStore } = await import('@reduxjs/toolkit')
+
+  const reducer: Redux.Reducer = (state = {}, _action) => state
+
+  beforeEach(() => {
+    vi.clearAllMocks()
+  })
 
   describe('given a function reducer', () => {
     it('calls createStore with the reducer', () => {
       configureStore({ reducer })
       expect(configureStore({ reducer })).toBeInstanceOf(Object)
-      expect(redux.applyMiddleware).toHaveBeenCalled()
-      expect(devtools.composeWithDevTools).toHaveBeenCalled() // @remap-prod-remove-line
+
       expect(redux.createStore).toHaveBeenCalledWith(
         reducer,
         undefined,
         expect.any(Function)
       )
+      expect(redux.applyMiddleware).toHaveBeenCalled()
+      expect(mockDevtoolsCompose).toHaveBeenCalled() // @remap-prod-remove-line
     })
   })
 
@@ -39,7 +90,7 @@ describe('configureStore', () => {
       expect(configureStore({ reducer })).toBeInstanceOf(Object)
       expect(redux.combineReducers).toHaveBeenCalledWith(reducer)
       expect(redux.applyMiddleware).toHaveBeenCalled()
-      expect(devtools.composeWithDevTools).toHaveBeenCalled() // @remap-prod-remove-line-line
+      expect(mockDevtoolsCompose).toHaveBeenCalled() // @remap-prod-remove-line-line
       expect(redux.createStore).toHaveBeenCalledWith(
         expect.any(Function),
         undefined,
@@ -51,16 +102,18 @@ describe('configureStore', () => {
   describe('given no reducer', () => {
     it('throws', () => {
       expect(configureStore).toThrow(
-        '"reducer" is a required argument, and must be a function or an object of functions that can be passed to combineReducers'
+        '`reducer` is a required argument, and must be a function or an object of functions that can be passed to combineReducers'
       )
     })
   })
 
   describe('given no middleware', () => {
     it('calls createStore without any middleware', () => {
-      expect(configureStore({ middleware: [], reducer })).toBeInstanceOf(Object)
+      expect(
+        configureStore({ middleware: () => new Tuple(), reducer })
+      ).toBeInstanceOf(Object)
       expect(redux.applyMiddleware).toHaveBeenCalledWith()
-      expect(devtools.composeWithDevTools).toHaveBeenCalled() // @remap-prod-remove-line-line
+      expect(mockDevtoolsCompose).toHaveBeenCalled() // @remap-prod-remove-line-line
       expect(redux.createStore).toHaveBeenCalledWith(
         reducer,
         undefined,
@@ -69,6 +122,15 @@ describe('configureStore', () => {
     })
   })
 
+  describe('given an array of middleware', () => {
+    it('throws an error requiring a callback', () => {
+      // @ts-expect-error
+      expect(() => configureStore({ middleware: [], reducer })).toThrow(
+        '`middleware` field must be a callback'
+      )
+    })
+  })
+
   describe('given undefined middleware', () => {
     it('calls createStore with default middleware', () => {
       expect(configureStore({ middleware: undefined, reducer })).toBeInstanceOf(
@@ -80,7 +142,7 @@ describe('configureStore', () => {
         expect.any(Function), // serializableCheck
         expect.any(Function) // actionCreatorCheck
       )
-      expect(devtools.composeWithDevTools).toHaveBeenCalled() // @remap-prod-remove-line-line
+      expect(mockDevtoolsCompose).toHaveBeenCalled() // @remap-prod-remove-line-line
       expect(redux.createStore).toHaveBeenCalledWith(
         reducer,
         undefined,
@@ -91,7 +153,7 @@ describe('configureStore', () => {
 
   describe('given a middleware creation function that returns undefined', () => {
     it('throws an error', () => {
-      const invalidBuilder = jest.fn((getDefaultMiddleware) => undefined as any)
+      const invalidBuilder = vi.fn((getDefaultMiddleware) => undefined as any)
       expect(() =>
         configureStore({ middleware: invalidBuilder, reducer })
       ).toThrow(
@@ -102,30 +164,22 @@ describe('configureStore', () => {
 
   describe('given a middleware creation function that returns an array with non-functions', () => {
     it('throws an error', () => {
-      const invalidBuilder = jest.fn((getDefaultMiddleware) => [true] as any)
+      const invalidBuilder = vi.fn((getDefaultMiddleware) => [true] as any)
       expect(() =>
         configureStore({ middleware: invalidBuilder, reducer })
       ).toThrow('each middleware provided to configureStore must be a function')
     })
   })
 
-  describe('given custom middleware that contains non-functions', () => {
-    it('throws an error', () => {
-      expect(() =>
-        configureStore({ middleware: [true] as any, reducer })
-      ).toThrow('each middleware provided to configureStore must be a function')
-    })
-  })
-
   describe('given custom middleware', () => {
     it('calls createStore with custom middleware and without default middleware', () => {
-      const thank: redux.Middleware = (_store) => (next) => (action) =>
+      const thank: Redux.Middleware = (_store) => (next) => (action) =>
         next(action)
-      expect(configureStore({ middleware: [thank], reducer })).toBeInstanceOf(
-        Object
-      )
+      expect(
+        configureStore({ middleware: () => new Tuple(thank), reducer })
+      ).toBeInstanceOf(Object)
       expect(redux.applyMiddleware).toHaveBeenCalledWith(thank)
-      expect(devtools.composeWithDevTools).toHaveBeenCalled() // @remap-prod-remove-line-line
+      expect(mockDevtoolsCompose).toHaveBeenCalled() // @remap-prod-remove-line-line
       expect(redux.createStore).toHaveBeenCalledWith(
         reducer,
         undefined,
@@ -136,15 +190,15 @@ describe('configureStore', () => {
 
   describe('middleware builder notation', () => {
     it('calls builder, passes getDefaultMiddleware and uses returned middlewares', () => {
-      const thank = jest.fn(
-        ((_store) => (next) => (action) => 'foobar') as redux.Middleware
+      const thank = vi.fn(
+        ((_store) => (next) => (action) => 'foobar') as Redux.Middleware
       )
 
-      const builder = jest.fn((getDefaultMiddleware) => {
+      const builder = vi.fn((getDefaultMiddleware) => {
         expect(getDefaultMiddleware).toEqual(expect.any(Function))
         expect(getDefaultMiddleware()).toEqual(expect.any(Array))
 
-        return [thank]
+        return new Tuple(thank)
       })
 
       const store = configureStore({ middleware: builder, reducer })
@@ -180,7 +234,7 @@ describe('configureStore', () => {
         Object
       )
       expect(redux.applyMiddleware).toHaveBeenCalled()
-      expect(devtools.composeWithDevTools).toHaveBeenCalledWith(options) // @remap-prod-remove-line
+      expect(mockDevtoolsCompose).toHaveBeenCalledWith(options) // @remap-prod-remove-line
       expect(redux.createStore).toHaveBeenCalledWith(
         reducer,
         undefined,
@@ -193,7 +247,7 @@ describe('configureStore', () => {
     it('calls createStore with preloadedState', () => {
       expect(configureStore({ reducer })).toBeInstanceOf(Object)
       expect(redux.applyMiddleware).toHaveBeenCalled()
-      expect(devtools.composeWithDevTools).toHaveBeenCalled() // @remap-prod-remove-line
+      expect(mockDevtoolsCompose).toHaveBeenCalled() // @remap-prod-remove-line
       expect(redux.createStore).toHaveBeenCalledWith(
         reducer,
         undefined,
@@ -203,39 +257,87 @@ describe('configureStore', () => {
   })
 
   describe('given enhancers', () => {
+    let dummyEnhancerCalled = false
+
+    const dummyEnhancer: StoreEnhancer =
+      (createStore) => (reducer, preloadedState) => {
+        dummyEnhancerCalled = true
+
+        return createStore(reducer, preloadedState)
+      }
+
+    beforeEach(() => {
+      dummyEnhancerCalled = false
+    })
+
     it('calls createStore with enhancers', () => {
-      const enhancer: redux.StoreEnhancer = (next) => next
-      expect(configureStore({ enhancers: [enhancer], reducer })).toBeInstanceOf(
-        Object
-      )
+      expect(
+        configureStore({
+          enhancers: (gDE) => gDE().concat(dummyEnhancer),
+          reducer,
+        })
+      ).toBeInstanceOf(Object)
       expect(redux.applyMiddleware).toHaveBeenCalled()
-      expect(devtools.composeWithDevTools).toHaveBeenCalled() // @remap-prod-remove-line
+      expect(mockDevtoolsCompose).toHaveBeenCalled() // @remap-prod-remove-line
       expect(redux.createStore).toHaveBeenCalledWith(
         reducer,
         undefined,
         expect.any(Function)
       )
+
+      expect(dummyEnhancerCalled).toBe(true)
     })
 
-    it('accepts a callback for customizing enhancers', () => {
-      let dummyEnhancerCalled = false
+    describe('invalid arguments', () => {
+      test('enhancers is not a callback', () => {
+        expect(() => configureStore({ reducer, enhancers: [] as any })).toThrow(
+          '`enhancers` field must be a callback'
+        )
+      })
+
+      test('callback fails to return array', () => {
+        expect(() =>
+          configureStore({ reducer, enhancers: (() => {}) as any })
+        ).toThrow('`enhancers` callback must return an array')
+      })
 
-      const dummyEnhancer: StoreEnhancer =
-        (createStore: StoreEnhancerStoreCreator) =>
-        (reducer, ...args: any[]) => {
-          dummyEnhancerCalled = true
+      test('array contains non-function', () => {
+        expect(() =>
+          configureStore({ reducer, enhancers: (() => ['']) as any })
+        ).toThrow('each enhancer provided to configureStore must be a function')
+      })
+    })
 
-          return createStore(reducer, ...args)
-        }
+    const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
+    beforeEach(() => {
+      consoleSpy.mockClear()
+    })
+    afterAll(() => {
+      consoleSpy.mockRestore()
+    })
 
-      const reducer = () => ({})
+    it('warns if middleware enhancer is excluded from final array when middlewares are provided', () => {
+      const store = configureStore({
+        reducer,
+        enhancers: () => new Tuple(dummyEnhancer),
+      })
+
+      expect(dummyEnhancerCalled).toBe(true)
 
+      expect(consoleSpy).toHaveBeenCalledWith(
+        'middlewares were provided, but middleware enhancer was not included in final enhancers - make sure to call `getDefaultEnhancers`'
+      )
+    })
+    it("doesn't warn when middleware enhancer is excluded if no middlewares provided", () => {
       const store = configureStore({
         reducer,
-        enhancers: (defaultEnhancers) => defaultEnhancers.concat(dummyEnhancer),
+        middleware: () => new Tuple(),
+        enhancers: () => new Tuple(dummyEnhancer),
       })
 
       expect(dummyEnhancerCalled).toBe(true)
+
+      expect(consoleSpy).not.toHaveBeenCalled()
     })
   })
 })
diff --git a/packages/toolkit/src/tests/configureStore.typetest.ts b/packages/toolkit/src/tests/configureStore.typetest.ts
--- a/packages/toolkit/src/tests/configureStore.typetest.ts
+++ b/packages/toolkit/src/tests/configureStore.typetest.ts
@@ -1,22 +1,18 @@
 /* eslint-disable no-lone-blocks */
 import type {
   Dispatch,
-  AnyAction,
+  UnknownAction,
   Middleware,
   Reducer,
   Store,
   Action,
   StoreEnhancer,
 } from 'redux'
-import { applyMiddleware } from 'redux'
+import { applyMiddleware, combineReducers } from 'redux'
 import type { PayloadAction, ConfigureStoreOptions } from '@reduxjs/toolkit'
-import {
-  configureStore,
-  getDefaultMiddleware,
-  createSlice,
-} from '@reduxjs/toolkit'
+import { configureStore, createSlice, Tuple } from '@reduxjs/toolkit'
 import type { ThunkMiddleware, ThunkAction, ThunkDispatch } from 'redux-thunk'
-import thunk from 'redux-thunk'
+import { thunk } from 'redux-thunk'
 import { expectNotAny, expectType } from './helpers'
 
 const _anyMiddleware: any = () => () => () => {}
@@ -52,10 +48,10 @@ const _anyMiddleware: any = () => () => () => {}
 {
   const reducer: Reducer<number> = () => 0
   const store = configureStore({ reducer })
-  const numberStore: Store<number, AnyAction> = store
+  const numberStore: Store<number, UnknownAction> = store
 
   // @ts-expect-error
-  const stringStore: Store<string, AnyAction> = store
+  const stringStore: Store<string, UnknownAction> = store
 }
 
 /*
@@ -71,20 +67,26 @@ const _anyMiddleware: any = () => () => () => {}
 }
 
 /*
- * Test: configureStore() accepts middleware array.
+ * Test: configureStore() accepts Tuple, but not plain array.
  */
 {
   const middleware: Middleware = (store) => (next) => next
 
   configureStore({
     reducer: () => 0,
-    middleware: [middleware],
+    middleware: () => new Tuple(middleware),
   })
 
   configureStore({
     reducer: () => 0,
     // @ts-expect-error
-    middleware: ['not middleware'],
+    middleware: () => [middleware],
+  })
+
+  configureStore({
+    reducer: () => 0,
+    // @ts-expect-error
+    middleware: () => new Tuple('not middleware'),
   })
 }
 
@@ -130,23 +132,31 @@ const _anyMiddleware: any = () => () => () => {}
   })
 
   configureStore({
-    reducer: () => 0,
     // @ts-expect-error
+    reducer: (_: number) => 0,
     preloadedState: 'non-matching state type',
   })
 }
 
 /*
- * Test: configureStore() accepts store enhancer.
+ * Test: configureStore() accepts store Tuple, but not plain array
  */
 {
   {
+    const enhancer = applyMiddleware(() => (next) => next)
+
     const store = configureStore({
       reducer: () => 0,
-      enhancers: [applyMiddleware(() => (next) => next)],
+      enhancers: () => new Tuple(enhancer),
     })
 
-    expectType<Dispatch & ThunkDispatch<number, undefined, AnyAction>>(
+    const store2 = configureStore({
+      reducer: () => 0,
+      // @ts-expect-error
+      enhancers: () => [enhancer],
+    })
+
+    expectType<Dispatch & ThunkDispatch<number, undefined, UnknownAction>>(
       store.dispatch
     )
   }
@@ -154,13 +164,13 @@ const _anyMiddleware: any = () => () => () => {}
   configureStore({
     reducer: () => 0,
     // @ts-expect-error
-    enhancers: ['not a store enhancer'],
+    enhancers: () => new Tuple('not a store enhancer'),
   })
 
   {
-    type SomePropertyStoreEnhancer = StoreEnhancer<{ someProperty: string }>
-
-    const somePropertyStoreEnhancer: SomePropertyStoreEnhancer = (next) => {
+    const somePropertyStoreEnhancer: StoreEnhancer<{ someProperty: string }> = (
+      next
+    ) => {
       return (reducer, preloadedState) => {
         return {
           ...next(reducer, preloadedState),
@@ -169,13 +179,9 @@ const _anyMiddleware: any = () => () => () => {}
       }
     }
 
-    type AnotherPropertyStoreEnhancer = StoreEnhancer<{
+    const anotherPropertyStoreEnhancer: StoreEnhancer<{
       anotherProperty: number
-    }>
-
-    const anotherPropertyStoreEnhancer: AnotherPropertyStoreEnhancer = (
-      next
-    ) => {
+    }> = (next) => {
       return (reducer, preloadedState) => {
         return {
           ...next(reducer, preloadedState),
@@ -186,24 +192,23 @@ const _anyMiddleware: any = () => () => () => {}
 
     const store = configureStore({
       reducer: () => 0,
-      enhancers: [somePropertyStoreEnhancer, anotherPropertyStoreEnhancer],
+      enhancers: () =>
+        new Tuple(somePropertyStoreEnhancer, anotherPropertyStoreEnhancer),
     })
 
-    expectType<Dispatch & ThunkDispatch<number, undefined, AnyAction>>(
-      store.dispatch
-    )
+    expectType<Dispatch>(store.dispatch)
     expectType<string>(store.someProperty)
     expectType<number>(store.anotherProperty)
 
     const storeWithCallback = configureStore({
       reducer: () => 0,
-      enhancers: (defaultEnhancers) =>
-        defaultEnhancers
+      enhancers: (getDefaultEnhancers) =>
+        getDefaultEnhancers()
           .prepend(anotherPropertyStoreEnhancer)
           .concat(somePropertyStoreEnhancer),
     })
 
-    expectType<Dispatch & ThunkDispatch<number, undefined, AnyAction>>(
+    expectType<Dispatch & ThunkDispatch<number, undefined, UnknownAction>>(
       store.dispatch
     )
     expectType<string>(storeWithCallback.someProperty)
@@ -211,11 +216,11 @@ const _anyMiddleware: any = () => () => () => {}
   }
 
   {
-    type StateExtendingEnhancer = StoreEnhancer<{}, { someProperty: string }>
-
-    const someStateExtendingEnhancer: StateExtendingEnhancer =
+    const someStateExtendingEnhancer: StoreEnhancer<
+      {},
+      { someProperty: string }
+    > =
       (next) =>
-      // @ts-expect-error how do you properly return an enhancer that extends state?
       (...args) => {
         const store = next(...args)
         const getState = () => ({
@@ -225,17 +230,14 @@ const _anyMiddleware: any = () => () => () => {}
         return {
           ...store,
           getState,
-        }
+        } as any
       }
 
-    type AnotherStateExtendingEnhancer = StoreEnhancer<
+    const anotherStateExtendingEnhancer: StoreEnhancer<
       {},
       { anotherProperty: number }
-    >
-
-    const anotherStateExtendingEnhancer: AnotherStateExtendingEnhancer =
+    > =
       (next) =>
-      // @ts-expect-error any input on this would be great
       (...args) => {
         const store = next(...args)
         const getState = () => ({
@@ -245,28 +247,24 @@ const _anyMiddleware: any = () => () => () => {}
         return {
           ...store,
           getState,
-        }
+        } as any
       }
 
     const store = configureStore({
       reducer: () => ({ aProperty: 0 }),
-      enhancers: [
-        someStateExtendingEnhancer,
-        anotherStateExtendingEnhancer,
-        // this doesn't work without the as const
-      ] as const,
+      enhancers: () =>
+        new Tuple(someStateExtendingEnhancer, anotherStateExtendingEnhancer),
     })
 
     const state = store.getState()
-
     expectType<number>(state.aProperty)
     expectType<string>(state.someProperty)
     expectType<number>(state.anotherProperty)
 
     const storeWithCallback = configureStore({
       reducer: () => ({ aProperty: 0 }),
-      enhancers: (dE) =>
-        dE.concat(someStateExtendingEnhancer, anotherStateExtendingEnhancer),
+      enhancers: (gDE) =>
+        gDE().concat(someStateExtendingEnhancer, anotherStateExtendingEnhancer),
     })
 
     const stateWithCallback = storeWithCallback.getState()
@@ -278,25 +276,162 @@ const _anyMiddleware: any = () => () => () => {}
 }
 
 /**
- * Test: configureStore() state type inference works when specifying both a
- * reducer object and a partial preloaded state.
+ * Test: Preloaded state typings
  */
 {
   let counterReducer1: Reducer<number> = () => 0
   let counterReducer2: Reducer<number> = () => 0
 
-  const store = configureStore({
-    reducer: {
-      counter1: counterReducer1,
-      counter2: counterReducer2,
-    },
-    preloadedState: {
-      counter1: 0,
-    },
-  })
+  /**
+   * Test: partial preloaded state
+   */
+  {
+    const store = configureStore({
+      reducer: {
+        counter1: counterReducer1,
+        counter2: counterReducer2,
+      },
+      preloadedState: {
+        counter1: 0,
+      },
+    })
 
-  const counter1: number = store.getState().counter1
-  const counter2: number = store.getState().counter2
+    const counter1: number = store.getState().counter1
+    const counter2: number = store.getState().counter2
+  }
+
+  /**
+   * Test: empty preloaded state
+   */
+  {
+    const store = configureStore({
+      reducer: {
+        counter1: counterReducer1,
+        counter2: counterReducer2,
+      },
+      preloadedState: {},
+    })
+
+    const counter1: number = store.getState().counter1
+    const counter2: number = store.getState().counter2
+  }
+
+  /**
+   * Test: excess properties in preloaded state
+   */
+  {
+    const store = configureStore({
+      reducer: {
+        // @ts-expect-error
+        counter1: counterReducer1,
+        counter2: counterReducer2,
+      },
+      preloadedState: {
+        counter1: 0,
+        counter3: 5,
+      },
+    })
+
+    const counter1: number = store.getState().counter1
+    const counter2: number = store.getState().counter2
+  }
+
+  /**
+   * Test: mismatching properties in preloaded state
+   */
+  {
+    const store = configureStore({
+      reducer: {
+        // @ts-expect-error
+        counter1: counterReducer1,
+        counter2: counterReducer2,
+      },
+      preloadedState: {
+        counter3: 5,
+      },
+    })
+
+    const counter1: number = store.getState().counter1
+    const counter2: number = store.getState().counter2
+  }
+
+  /**
+   * Test: string preloaded state when expecting object
+   */
+  {
+    const store = configureStore({
+      reducer: {
+        // @ts-expect-error
+        counter1: counterReducer1,
+        counter2: counterReducer2,
+      },
+      preloadedState: 'test',
+    })
+
+    const counter1: number = store.getState().counter1
+    const counter2: number = store.getState().counter2
+  }
+
+  /**
+   * Test: nested combineReducers allows partial
+   */
+  {
+    const store = configureStore({
+      reducer: {
+        group1: combineReducers({
+          counter1: counterReducer1,
+          counter2: counterReducer2,
+        }),
+        group2: combineReducers({
+          counter1: counterReducer1,
+          counter2: counterReducer2,
+        }),
+      },
+      preloadedState: {
+        group1: {
+          counter1: 5,
+        },
+      },
+    })
+
+    const group1counter1: number = store.getState().group1.counter1
+    const group1counter2: number = store.getState().group1.counter2
+    const group2counter1: number = store.getState().group2.counter1
+    const group2counter2: number = store.getState().group2.counter2
+  }
+
+  /**
+   * Test: non-nested combineReducers does not allow partial
+   */
+  {
+    interface GroupState {
+      counter1: number
+      counter2: number
+    }
+
+    const initialState = { counter1: 0, counter2: 0 }
+
+    const group1Reducer: Reducer<GroupState> = (state = initialState) => state
+    const group2Reducer: Reducer<GroupState> = (state = initialState) => state
+
+    const store = configureStore({
+      reducer: {
+        // @ts-expect-error
+        group1: group1Reducer,
+        group2: group2Reducer,
+      },
+      preloadedState: {
+        group1: {
+          counter1: 5,
+        },
+      },
+    })
+
+    const group1counter1: number = store.getState().group1.counter1
+    const group1counter2: number = store.getState().group1.counter2
+    const group2counter1: number = store.getState().group2.counter1
+    const group2counter2: number = store.getState().group2.counter2
+  }
 }
 
 /**
@@ -385,7 +520,7 @@ const _anyMiddleware: any = () => () => () => {}
   {
     const store = configureStore({
       reducer: reducerA,
-      middleware: [],
+      middleware: () => new Tuple(),
     })
     // @ts-expect-error
     store.dispatch(thunkA())
@@ -398,21 +533,8 @@ const _anyMiddleware: any = () => () => () => {}
   {
     const store = configureStore({
       reducer: reducerA,
-      middleware: [thunk] as [ThunkMiddleware<StateA>],
-    })
-    store.dispatch(thunkA())
-    // @ts-expect-error
-    store.dispatch(thunkB())
-  }
-  /**
-   * Test: using getDefaultMiddleware
-   */
-  {
-    const store = configureStore({
-      reducer: reducerA,
-      middleware: getDefaultMiddleware<StateA>(),
+      middleware: () => new Tuple(thunk as ThunkMiddleware<StateA>),
     })
-
     store.dispatch(thunkA())
     // @ts-expect-error
     store.dispatch(thunkB())
@@ -423,21 +545,8 @@ const _anyMiddleware: any = () => () => () => {}
   {
     const store = configureStore({
       reducer: reducerA,
-      middleware: [] as any as [Middleware<(a: StateA) => boolean, StateA>],
-    })
-    const result: boolean = store.dispatch(5)
-    // @ts-expect-error
-    const result2: string = store.dispatch(5)
-  }
-  /**
-   * Test: read-only middleware tuple
-   */
-  {
-    const store = configureStore({
-      reducer: reducerA,
-      middleware: [] as any as readonly [
-        Middleware<(a: StateA) => boolean, StateA>
-      ],
+      middleware: () =>
+        new Tuple(0 as unknown as Middleware<(a: StateA) => boolean, StateA>),
     })
     const result: boolean = store.dispatch(5)
     // @ts-expect-error
@@ -447,14 +556,16 @@ const _anyMiddleware: any = () => () => () => {}
    * Test: multiple custom middleware
    */
   {
-    const middleware = [] as any as [
-      Middleware<(a: 'a') => 'A', StateA>,
-      Middleware<(b: 'b') => 'B', StateA>,
-      ThunkMiddleware<StateA>
-    ]
+    const middleware = [] as any as Tuple<
+      [
+        Middleware<(a: 'a') => 'A', StateA>,
+        Middleware<(b: 'b') => 'B', StateA>,
+        ThunkMiddleware<StateA>
+      ]
+    >
     const store = configureStore({
       reducer: reducerA,
-      middleware,
+      middleware: () => middleware,
     })
 
     const result: 'A' = store.dispatch('a')
@@ -471,30 +582,38 @@ const _anyMiddleware: any = () => () => () => {}
       void,
       {},
       undefined,
-      AnyAction
+      UnknownAction
     >)
     // `null` for the `extra` generic was previously documented in the RTK "Advanced Tutorial", but
     // is a bad pattern and users should use `unknown` instead
     // @ts-expect-error
-    store.dispatch(function () {} as ThunkAction<void, {}, null, AnyAction>)
+    store.dispatch(function () {} as ThunkAction<void, {}, null, UnknownAction>)
     // unknown is the best way to type a ThunkAction if you do not care
     // about the value of the extraArgument, as it will always work with every
     // ThunkMiddleware, no matter the actual extraArgument type
-    store.dispatch(function () {} as ThunkAction<void, {}, unknown, AnyAction>)
+    store.dispatch(function () {} as ThunkAction<
+      void,
+      {},
+      unknown,
+      UnknownAction
+    >)
     // @ts-expect-error
-    store.dispatch(function () {} as ThunkAction<void, {}, boolean, AnyAction>)
+    store.dispatch(function () {} as ThunkAction<
+      void,
+      {},
+      boolean,
+      UnknownAction
+    >)
   }
 
   /**
    * Test: custom middleware and getDefaultMiddleware
    */
   {
-    const middleware = getDefaultMiddleware<StateA>().prepend(
-      (() => {}) as any as Middleware<(a: 'a') => 'A', StateA>
-    )
     const store = configureStore({
       reducer: reducerA,
-      middleware,
+      middleware: (gDM) =>
+        gDM().prepend((() => {}) as any as Middleware<(a: 'a') => 'A', StateA>),
     })
 
     const result1: 'A' = store.dispatch('a')
@@ -508,15 +627,19 @@ const _anyMiddleware: any = () => () => () => {}
    */
   {
     const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> = _anyMiddleware
-    const concatenated = getDefaultMiddleware<StateA>().prepend(otherMiddleware)
-
-    expectType<
-      ReadonlyArray<typeof otherMiddleware | ThunkMiddleware | Middleware<{}>>
-    >(concatenated)
 
     const store = configureStore({
       reducer: reducerA,
-      middleware: concatenated,
+      middleware: (gDM) => {
+        const concatenated = gDM().prepend(otherMiddleware)
+        expectType<
+          ReadonlyArray<
+            typeof otherMiddleware | ThunkMiddleware | Middleware<{}>
+          >
+        >(concatenated)
+
+        return concatenated
+      },
     })
     const result1: 'A' = store.dispatch('a')
     const result2: Promise<'A'> = store.dispatch(thunkA())
@@ -529,15 +652,19 @@ const _anyMiddleware: any = () => () => () => {}
    */
   {
     const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> = _anyMiddleware
-    const concatenated = getDefaultMiddleware<StateA>().concat(otherMiddleware)
-
-    expectType<
-      ReadonlyArray<typeof otherMiddleware | ThunkMiddleware | Middleware<{}>>
-    >(concatenated)
 
     const store = configureStore({
       reducer: reducerA,
-      middleware: concatenated,
+      middleware: (gDM) => {
+        const concatenated = gDM().concat(otherMiddleware)
+
+        expectType<
+          ReadonlyArray<
+            typeof otherMiddleware | ThunkMiddleware | Middleware<{}>
+          >
+        >(concatenated)
+        return concatenated
+      },
     })
     const result1: 'A' = store.dispatch('a')
     const result2: Promise<'A'> = store.dispatch(thunkA())
@@ -682,8 +809,8 @@ const _anyMiddleware: any = () => () => () => {}
     // the thunk middleware type kicks in and TS thinks a plain action is being returned
     expectType<
       ((action: Action<'actionListenerMiddleware/add'>) => Unsubscribe) &
-        ThunkDispatch<CounterState, undefined, AnyAction> &
-        Dispatch<AnyAction>
+        ThunkDispatch<CounterState, undefined, UnknownAction> &
+        Dispatch<UnknownAction>
     >(store.dispatch)
 
     const unsubscribe = store.dispatch({
diff --git a/packages/toolkit/src/tests/createAction.test.ts b/packages/toolkit/src/tests/createAction.test.ts
--- a/packages/toolkit/src/tests/createAction.test.ts
+++ b/packages/toolkit/src/tests/createAction.test.ts
@@ -1,4 +1,4 @@
-import { createAction, getType, isAction } from '@reduxjs/toolkit'
+import { createAction, isActionCreator } from '@reduxjs/toolkit'
 
 describe('createAction', () => {
   it('should create an action', () => {
@@ -122,30 +122,27 @@ describe('createAction', () => {
   })
 })
 
-describe('isAction', () => {
-  it('should only return true for plain objects with a type property', () => {
-    const actionCreator = createAction('anAction')
-    class Action {
-      type = 'totally an action'
-    }
-    const testCases: [action: unknown, expected: boolean][] = [
-      [{ type: 'an action' }, true],
-      [{ type: 'more props', extra: true }, true],
-      [actionCreator(), true],
-      [actionCreator, false],
-      [Promise.resolve({ type: 'an action' }), false],
-      [new Action(), false],
-      ['a string', false],
+const actionCreator = createAction('anAction')
+
+class Action {
+  type = 'totally an action'
+}
+
+describe('isActionCreator', () => {
+  it('should only return true for action creators', () => {
+    expect(isActionCreator(actionCreator)).toBe(true)
+    const notActionCreators = [
+      { type: 'an action' },
+      { type: 'more props', extra: true },
+      actionCreator(),
+      Promise.resolve({ type: 'an action' }),
+      new Action(),
+      false,
+      'a string',
+      false,
     ]
-    for (const [action, expected] of testCases) {
-      expect(isAction(action)).toBe(expected)
+    for (const notActionCreator of notActionCreators) {
+      expect(isActionCreator(notActionCreator)).toBe(false)
     }
   })
 })
-
-describe('getType', () => {
-  it('should return the action type', () => {
-    const actionCreator = createAction('A_TYPE')
-    expect(getType(actionCreator)).toEqual('A_TYPE')
-  })
-})
diff --git a/packages/toolkit/src/tests/createAction.typetest.tsx b/packages/toolkit/src/tests/createAction.typetest.tsx
--- a/packages/toolkit/src/tests/createAction.typetest.tsx
+++ b/packages/toolkit/src/tests/createAction.typetest.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import type { Action, AnyAction, ActionCreator } from 'redux'
+import type { Action, UnknownAction, ActionCreator } from 'redux'
 import type {
   PayloadAction,
   PayloadActionCreator,
@@ -92,7 +92,7 @@ import { expectType } from './helpers'
     }),
     { type: 'action' }
   ) as PayloadActionCreator
-  const actionCreator: ActionCreator<AnyAction> = payloadActionCreator
+  const actionCreator: ActionCreator<UnknownAction> = payloadActionCreator
 
   const payloadActionCreator2 = Object.assign(
     (payload?: number) => ({
@@ -258,7 +258,7 @@ import { expectType } from './helpers'
   // simple use case
   {
     const actionCreator = createAction<string, 'test'>('test')
-    const x: Action<unknown> = {} as any
+    const x: Action<string> = {} as any
     if (actionCreator.match(x)) {
       expectType<'test'>(x.type)
       expectType<string>(x.payload)
@@ -273,7 +273,7 @@ import { expectType } from './helpers'
   // special case: optional argument
   {
     const actionCreator = createAction<string | undefined, 'test'>('test')
-    const x: Action<unknown> = {} as any
+    const x: Action<string> = {} as any
     if (actionCreator.match(x)) {
       expectType<'test'>(x.type)
       expectType<string | undefined>(x.payload)
@@ -283,7 +283,7 @@ import { expectType } from './helpers'
   // special case: without argument
   {
     const actionCreator = createAction('test')
-    const x: Action<unknown> = {} as any
+    const x: Action<string> = {} as any
     if (actionCreator.match(x)) {
       expectType<'test'>(x.type)
       // @ts-expect-error
@@ -298,7 +298,7 @@ import { expectType } from './helpers'
       meta: '',
       error: false,
     }))
-    const x: Action<unknown> = {} as any
+    const x: Action<string> = {} as any
     if (actionCreator.match(x)) {
       expectType<'test'>(x.type)
       expectType<string>(x.payload)
@@ -315,7 +315,7 @@ import { expectType } from './helpers'
   // potential use: as array filter
   {
     const actionCreator = createAction<string, 'test'>('test')
-    const x: Array<Action<unknown>> = []
+    const x: Array<Action<string>> = []
     expectType<Array<PayloadAction<string, 'test'>>>(
       x.filter(actionCreator.match)
     )
diff --git a/packages/toolkit/src/tests/createAsyncThunk.test.ts b/packages/toolkit/src/tests/createAsyncThunk.test.ts
--- a/packages/toolkit/src/tests/createAsyncThunk.test.ts
+++ b/packages/toolkit/src/tests/createAsyncThunk.test.ts
@@ -1,4 +1,5 @@
-import type { AnyAction } from '@reduxjs/toolkit'
+import { vi } from 'vitest'
+import type { UnknownAction } from '@reduxjs/toolkit'
 import {
   createAsyncThunk,
   unwrapResult,
@@ -36,6 +37,20 @@ describe('createAsyncThunk', () => {
     expect(thunkActionCreator.typePrefix).toBe('testType')
   })
 
+  it('includes a settled matcher', () => {
+    const thunkActionCreator = createAsyncThunk('testType', async () => 42)
+    expect(thunkActionCreator.settled).toEqual(expect.any(Function))
+    expect(thunkActionCreator.settled(thunkActionCreator.pending(''))).toBe(
+      false
+    )
+    expect(
+      thunkActionCreator.settled(thunkActionCreator.rejected(null, ''))
+    ).toBe(true)
+    expect(
+      thunkActionCreator.settled(thunkActionCreator.fulfilled(42, ''))
+    ).toBe(true)
+  })
+
   it('works without passing arguments to the payload creator', async () => {
     const thunkActionCreator = createAsyncThunk('testType', async () => 42)
 
@@ -58,7 +73,7 @@ describe('createAsyncThunk', () => {
   })
 
   it('accepts arguments and dispatches the actions on resolve', async () => {
-    const dispatch = jest.fn()
+    const dispatch = vi.fn()
 
     let passedArg: any
 
@@ -98,7 +113,7 @@ describe('createAsyncThunk', () => {
   })
 
   it('accepts arguments and dispatches the actions on reject', async () => {
-    const dispatch = jest.fn()
+    const dispatch = vi.fn()
 
     const args = 123
     let generatedRequestId = ''
@@ -134,7 +149,7 @@ describe('createAsyncThunk', () => {
   })
 
   it('dispatches an empty error when throwing a random object without serializedError properties', async () => {
-    const dispatch = jest.fn()
+    const dispatch = vi.fn()
 
     const args = 123
     let generatedRequestId = ''
@@ -169,7 +184,7 @@ describe('createAsyncThunk', () => {
   })
 
   it('dispatches an action with a formatted error when throwing an object with known error keys', async () => {
-    const dispatch = jest.fn()
+    const dispatch = vi.fn()
 
     const args = 123
     let generatedRequestId = ''
@@ -210,7 +225,7 @@ describe('createAsyncThunk', () => {
   })
 
   it('dispatches a rejected action with a customized payload when a user returns rejectWithValue()', async () => {
-    const dispatch = jest.fn()
+    const dispatch = vi.fn()
 
     const args = 123
     let generatedRequestId = ''
@@ -255,7 +270,7 @@ describe('createAsyncThunk', () => {
   })
 
   it('dispatches a rejected action with a customized payload when a user throws rejectWithValue()', async () => {
-    const dispatch = jest.fn()
+    const dispatch = vi.fn()
 
     const args = 123
     let generatedRequestId = ''
@@ -300,7 +315,7 @@ describe('createAsyncThunk', () => {
   })
 
   it('dispatches a rejected action with a miniSerializeError when rejectWithValue conditions are not satisfied', async () => {
-    const dispatch = jest.fn()
+    const dispatch = vi.fn()
 
     const args = 123
     let generatedRequestId = ''
@@ -376,14 +391,14 @@ describe('createAsyncThunk with abortController', () => {
   )
 
   let store = configureStore({
-    reducer(store: AnyAction[] = []) {
+    reducer(store: UnknownAction[] = []) {
       return store
     },
   })
 
   beforeEach(() => {
     store = configureStore({
-      reducer(store: AnyAction[] = [], action) {
+      reducer(store: UnknownAction[] = [], action) {
         return [...store, action]
       },
     })
@@ -484,14 +499,14 @@ describe('createAsyncThunk with abortController', () => {
   describe('behaviour with missing AbortController', () => {
     let keepAbortController: typeof window['AbortController']
     let freshlyLoadedModule: typeof import('../createAsyncThunk')
-    let restore: () => void
+    let restore: () => void = () => {}
     let nodeEnv: string
 
-    beforeEach(() => {
+    beforeEach(async () => {
       keepAbortController = window.AbortController
       delete (window as any).AbortController
-      jest.resetModules()
-      freshlyLoadedModule = require('../createAsyncThunk')
+      vi.resetModules()
+      freshlyLoadedModule = await import('../createAsyncThunk')
       restore = mockConsole(createConsole())
       nodeEnv = process.env.NODE_ENV!
       ;(process.env as any).NODE_ENV = 'development'
@@ -501,10 +516,10 @@ describe('createAsyncThunk with abortController', () => {
       ;(process.env as any).NODE_ENV = nodeEnv
       restore()
       window.AbortController = keepAbortController
-      jest.resetModules()
+      vi.resetModules()
     })
 
-    test('calling `abort` on an asyncThunk works with a FallbackAbortController if no global abortController is not available', async () => {
+    test('calling a thunk made with createAsyncThunk should fail if no global abortController is not available', async () => {
       const longRunningAsyncThunk = freshlyLoadedModule.createAsyncThunk(
         'longRunning',
         async () => {
@@ -512,14 +527,7 @@ describe('createAsyncThunk with abortController', () => {
         }
       )
 
-      store.dispatch(longRunningAsyncThunk()).abort()
-      // should only log once, even if called twice
-      store.dispatch(longRunningAsyncThunk()).abort()
-
-      expect(getLog().log).toMatchInlineSnapshot(`
-        "This platform does not implement AbortController. 
-        If you want to use the AbortController to react to \`abort\` events, please consider importing a polyfill like 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'."
-      `)
+      expect(longRunningAsyncThunk()).toThrow('AbortController is not defined')
     })
   })
 })
@@ -539,10 +547,10 @@ test('non-serializable arguments are ignored by serializableStateInvariantMiddle
 
 describe('conditional skipping of asyncThunks', () => {
   const arg = {}
-  const getState = jest.fn(() => ({}))
-  const dispatch = jest.fn((x: any) => x)
-  const payloadCreator = jest.fn((x: typeof arg) => 10)
-  const condition = jest.fn(() => false)
+  const getState = vi.fn(() => ({}))
+  const dispatch = vi.fn((x: any) => x)
+  const payloadCreator = vi.fn((x: typeof arg) => 10)
+  const condition = vi.fn(() => false)
   const extra = {}
 
   beforeEach(() => {
@@ -645,7 +653,7 @@ describe('conditional skipping of asyncThunks', () => {
   })
 
   test('does not fail when attempting to abort a canceled promise', async () => {
-    const asyncPayloadCreator = jest.fn(async (x: typeof arg) => {
+    const asyncPayloadCreator = vi.fn(async (x: typeof arg) => {
       await delay(200)
       return 10
     })
@@ -720,8 +728,8 @@ test('serializeError implementation', async () => {
 })
 
 describe('unwrapResult', () => {
-  const getState = jest.fn(() => ({}))
-  const dispatch = jest.fn((x: any) => x)
+  const getState = vi.fn(() => ({}))
+  const dispatch = vi.fn((x: any) => x)
   const extra = {}
   test('fulfilled case', async () => {
     const asyncThunk = createAsyncThunk('test', () => {
@@ -779,7 +787,7 @@ describe('idGenerator option', () => {
   test('idGenerator implementation - can customizes how request IDs are generated', async () => {
     function makeFakeIdGenerator() {
       let id = 0
-      return jest.fn(() => {
+      return vi.fn(() => {
         id++
         return `fake-random-id-${id}`
       })
@@ -833,7 +841,7 @@ describe('idGenerator option', () => {
   })
 
   test('idGenerator should be called with thunkArg', async () => {
-    const customIdGenerator = jest.fn((seed) => `fake-unique-random-id-${seed}`)
+    const customIdGenerator = vi.fn((seed) => `fake-unique-random-id-${seed}`)
     let generatedRequestId = ''
     const asyncThunk = createAsyncThunk(
       'test',
@@ -855,7 +863,7 @@ describe('idGenerator option', () => {
 
 test('`condition` will see state changes from a synchronously invoked asyncThunk', () => {
   type State = ReturnType<typeof store.getState>
-  const onStart = jest.fn()
+  const onStart = vi.fn()
   const asyncThunk = createAsyncThunk<
     void,
     { force?: boolean },
@@ -981,6 +989,7 @@ describe('meta', () => {
     expect(thunk.fulfilled).toEqual(expectFunction)
     expect(thunk.pending).toEqual(expectFunction)
     expect(thunk.rejected).toEqual(expectFunction)
+    expect(thunk.settled).toEqual(expectFunction)
     expect(thunk.fulfilled.type).toBe('a/fulfilled')
   })
 })
diff --git a/packages/toolkit/src/tests/createAsyncThunk.typetest.ts b/packages/toolkit/src/tests/createAsyncThunk.typetest.ts
--- a/packages/toolkit/src/tests/createAsyncThunk.typetest.ts
+++ b/packages/toolkit/src/tests/createAsyncThunk.typetest.ts
@@ -1,5 +1,9 @@
 /* eslint-disable no-lone-blocks */
-import type { AnyAction, SerializedError, AsyncThunk } from '@reduxjs/toolkit'
+import type {
+  UnknownAction,
+  SerializedError,
+  AsyncThunk,
+} from '@reduxjs/toolkit'
 import {
   createAsyncThunk,
   createReducer,
@@ -20,8 +24,8 @@ import type {
 import type { TSVersion } from '@phryneas/ts-version'
 
 const ANY = {} as any
-const defaultDispatch = (() => {}) as ThunkDispatch<{}, any, AnyAction>
-const anyAction = { type: 'foo' } as AnyAction
+const defaultDispatch = (() => {}) as ThunkDispatch<{}, any, UnknownAction>
+const unknownAction = { type: 'foo' } as UnknownAction
 
 // basic usage
 ;(async function () {
@@ -91,7 +95,7 @@ const anyAction = { type: 'foo' } as AnyAction
   const correctDispatch = (() => {}) as ThunkDispatch<
     BookModel[],
     { userAPI: Function },
-    AnyAction
+    UnknownAction
   >
 
   // Verify that the the first type args to createAsyncThunk line up right
@@ -504,8 +508,8 @@ const anyAction = { type: 'foo' } as AnyAction
     serializeError: funkySerializeError,
   })
 
-  if (shouldWork.rejected.match(anyAction)) {
-    expectType<Funky>(anyAction.error)
+  if (shouldWork.rejected.match(unknownAction)) {
+    expectType<Funky>(unknownAction.error)
   }
 }
 
@@ -660,7 +664,7 @@ const anyAction = { type: 'foo' } as AnyAction
     // correct dispatch type
     const test2: number = api.dispatch((dispatch, getState) => {
       expectExactType<
-        ThunkDispatch<{ foo: { value: number } }, undefined, AnyAction>
+        ThunkDispatch<{ foo: { value: number } }, undefined, UnknownAction>
       >(ANY)(dispatch)
       expectExactType<() => { foo: { value: number } }>(ANY)(getState)
       return getState().foo.value
@@ -686,7 +690,7 @@ const anyAction = { type: 'foo' } as AnyAction
     // correct dispatch type
     const test2: number = api.dispatch((dispatch, getState) => {
       expectExactType<
-        ThunkDispatch<{ foo: { value: number } }, undefined, AnyAction>
+        ThunkDispatch<{ foo: { value: number } }, undefined, UnknownAction>
       >(ANY)(dispatch)
       expectExactType<() => { foo: { value: number } }>(ANY)(getState)
       return getState().foo.value
@@ -713,7 +717,7 @@ const anyAction = { type: 'foo' } as AnyAction
       // correct dispatch type
       const test2: number = api.dispatch((dispatch, getState) => {
         expectExactType<
-          ThunkDispatch<{ foo: { value: number } }, undefined, AnyAction>
+          ThunkDispatch<{ foo: { value: number } }, undefined, UnknownAction>
         >(ANY)(dispatch)
         expectExactType<() => { foo: { value: number } }>(ANY)(getState)
         return getState().foo.value
diff --git a/packages/toolkit/src/tests/createDraftSafeSelector.test.ts b/packages/toolkit/src/tests/createDraftSafeSelector.test.ts
--- a/packages/toolkit/src/tests/createDraftSafeSelector.test.ts
+++ b/packages/toolkit/src/tests/createDraftSafeSelector.test.ts
@@ -11,6 +11,13 @@ test('handles normal values correctly', () => {
   let state = { value: 1 }
   expect(unsafeSelector(state)).toBe(1)
   expect(draftSafeSelector(state)).toBe(1)
+  expect(draftSafeSelector).toHaveProperty('resultFunc')
+  expect(draftSafeSelector).toHaveProperty('memoizedResultFunc')
+  expect(draftSafeSelector).toHaveProperty('lastResult')
+  expect(draftSafeSelector).toHaveProperty('dependencies')
+  expect(draftSafeSelector).toHaveProperty('recomputations')
+  expect(draftSafeSelector).toHaveProperty('resetRecomputations')
+  expect(draftSafeSelector).toHaveProperty('clearCache')
 
   state = { value: 2 }
   expect(unsafeSelector(state)).toBe(2)
diff --git a/packages/toolkit/src/tests/createEntityAdapter.typetest.ts b/packages/toolkit/src/tests/createEntityAdapter.typetest.ts
--- a/packages/toolkit/src/tests/createEntityAdapter.typetest.ts
+++ b/packages/toolkit/src/tests/createEntityAdapter.typetest.ts
@@ -9,9 +9,9 @@ import type {
 import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'
 import { expectType } from './helpers'
 
-function extractReducers<T>(
-  adapter: EntityAdapter<T>
-): Omit<EntityStateAdapter<T>, 'map'> {
+function extractReducers<T, Id extends EntityId>(
+  adapter: EntityAdapter<T, Id>
+): Omit<EntityStateAdapter<T, Id>, 'map'> {
   const { selectId, sortComparer, getInitialState, getSelectors, ...rest } =
     adapter
   return rest
@@ -21,8 +21,9 @@ function extractReducers<T>(
  * should be usable in a slice, with all the "reducer-like" functions
  */
 {
+  type Id = string & { readonly __tag: unique symbol }
   type Entity = {
-    value: string
+    id: Id
   }
   const adapter = createEntityAdapter<Entity>()
   const slice = createSlice({
@@ -48,19 +49,21 @@ function extractReducers<T>(
     // @ts-expect-error
     slice.actions.setAll
   )
-  expectType<ActionCreatorWithPayload<EntityId>>(slice.actions.removeOne)
-  expectType<ActionCreatorWithPayload<ReadonlyArray<EntityId>>>(
+  expectType<ActionCreatorWithPayload<Id>>(slice.actions.removeOne)
+  expectType<ActionCreatorWithPayload<ReadonlyArray<Id>>>(
     slice.actions.removeMany
   )
   // @ts-expect-error
   expectType<ActionCreatorWithPayload<EntityId[]>>(slice.actions.removeMany)
   expectType<ActionCreatorWithoutPayload>(slice.actions.removeAll)
-  expectType<ActionCreatorWithPayload<Update<Entity>>>(slice.actions.updateOne)
-  expectType<ActionCreatorWithPayload<Update<Entity>[]>>(
+  expectType<ActionCreatorWithPayload<Update<Entity, Id>>>(
+    slice.actions.updateOne
+  )
+  expectType<ActionCreatorWithPayload<Update<Entity, Id>[]>>(
     // @ts-expect-error
     slice.actions.updateMany
   )
-  expectType<ActionCreatorWithPayload<ReadonlyArray<Update<Entity>>>>(
+  expectType<ActionCreatorWithPayload<ReadonlyArray<Update<Entity, Id>>>>(
     slice.actions.updateMany
   )
   expectType<ActionCreatorWithPayload<Entity>>(slice.actions.upsertOne)
@@ -78,9 +81,11 @@ function extractReducers<T>(
  */
 {
   type Entity = {
+    id: EntityId
     value: string
   }
   type Entity2 = {
+    id: EntityId
     value2: string
   }
   const adapter = createEntityAdapter<Entity>()
@@ -100,9 +105,7 @@ function extractReducers<T>(
  * should be usable in a slice with extra properties
  */
 {
-  type Entity = {
-    value: string
-  }
+  type Entity = { id: EntityId; value: string }
   const adapter = createEntityAdapter<Entity>()
   createSlice({
     name: 'test',
@@ -117,9 +120,7 @@ function extractReducers<T>(
  * should not be usable in a slice with an unfitting state
  */
 {
-  type Entity = {
-    value: string
-  }
+  type Entity = { id: EntityId; value: string }
   const adapter = createEntityAdapter<Entity>()
   createSlice({
     name: 'test',
@@ -130,3 +131,18 @@ function extractReducers<T>(
     },
   })
 }
+
+/**
+ * should not be able to create an adapter unless the type has an Id
+ * or an idSelector is provided
+ */
+{
+  type Entity = {
+    value: string
+  }
+  // @ts-expect-error
+  const adapter = createEntityAdapter<Entity>()
+  const adapter2: EntityAdapter<Entity, Entity['value']> = createEntityAdapter({
+    selectId: (e: Entity) => e.value,
+  })
+}
diff --git a/packages/toolkit/src/tests/createReducer.test.ts b/packages/toolkit/src/tests/createReducer.test.ts
--- a/packages/toolkit/src/tests/createReducer.test.ts
+++ b/packages/toolkit/src/tests/createReducer.test.ts
@@ -1,10 +1,12 @@
+import { vi } from 'vitest'
 import type {
   CaseReducer,
   PayloadAction,
   Draft,
   Reducer,
-  AnyAction,
+  UnknownAction,
 } from '@reduxjs/toolkit'
+import { isPlainObject } from '@reduxjs/toolkit'
 import { createReducer, createAction, createNextState } from '@reduxjs/toolkit'
 import {
   mockConsole,
@@ -27,11 +29,14 @@ interface ToggleTodoPayload {
 
 type TodoState = Todo[]
 type TodosReducer = Reducer<TodoState, PayloadAction<any>>
-type AddTodoReducer = CaseReducer<TodoState, PayloadAction<AddTodoPayload>>
+type AddTodoReducer = CaseReducer<
+  TodoState,
+  PayloadAction<AddTodoPayload, 'ADD_TODO'>
+>
 
 type ToggleTodoReducer = CaseReducer<
   TodoState,
-  PayloadAction<ToggleTodoPayload>
+  PayloadAction<ToggleTodoPayload, 'TOGGLE_TODO'>
 >
 
 type CreateReducer = typeof createReducer
@@ -58,9 +63,8 @@ describe('createReducer', () => {
       todo.completed = !todo.completed
     }
 
-    const todosReducer = createReducer([] as TodoState, {
-      ADD_TODO: addTodo,
-      TOGGLE_TODO: toggleTodo,
+    const todosReducer = createReducer([] as TodoState, (builder) => {
+      builder.addCase('ADD_TODO', addTodo).addCase('TOGGLE_TODO', toggleTodo)
     })
 
     behavesLikeReducer(todosReducer)
@@ -70,32 +74,38 @@ describe('createReducer', () => {
     let originalNodeEnv = process.env.NODE_ENV
 
     beforeEach(() => {
-      jest.resetModules()
+      vi.resetModules()
     })
 
     afterEach(() => {
       process.env.NODE_ENV = originalNodeEnv
     })
 
-    it('Warns about object notation deprecation, once', () => {
-      const { createReducer } = require('../createReducer')
-      let dummyReducer = (createReducer as CreateReducer)([] as TodoState, {})
+    it('Throws an error if the legacy object notation is used', async () => {
+      const { createReducer } = await import('../createReducer')
+      const wrapper = () => {
+        // @ts-ignore
+        let dummyReducer = (createReducer as CreateReducer)([] as TodoState, {})
+      }
 
-      expect(getLog().levels.warn).toMatch(
-        /The object notation for `createReducer` is deprecated/
+      expect(wrapper).toThrowError(
+        /The object notation for `createReducer` has been removed/
       )
-      restore = mockConsole(createConsole())
 
-      dummyReducer = (createReducer as CreateReducer)([] as TodoState, {})
-      expect(getLog().levels.warn).toBe('')
+      expect(wrapper).toThrowError(
+        /The object notation for `createReducer` has been removed/
+      )
     })
 
-    it('Does not warn in production', () => {
+    it('Crashes in production', async () => {
       process.env.NODE_ENV = 'production'
-      const { createReducer } = require('../createReducer')
-      let dummyReducer = (createReducer as CreateReducer)([] as TodoState, {})
+      const { createReducer } = await import('../createReducer')
+      const wrapper = () => {
+        // @ts-ignore
+        let dummyReducer = (createReducer as CreateReducer)([] as TodoState, {})
+      }
 
-      expect(getLog().levels.warn).toBe('')
+      expect(wrapper).toThrowError()
     })
   })
 
@@ -103,7 +113,7 @@ describe('createReducer', () => {
     let originalNodeEnv = process.env.NODE_ENV
 
     beforeEach(() => {
-      jest.resetModules()
+      vi.resetModules()
       process.env.NODE_ENV = 'production'
     })
 
@@ -111,8 +121,8 @@ describe('createReducer', () => {
       process.env.NODE_ENV = originalNodeEnv
     })
 
-    test('Freezes data in production', () => {
-      const { createReducer } = require('../createReducer')
+    test('Freezes data in production', async () => {
+      const { createReducer } = await import('../createReducer')
       const addTodo: AddTodoReducer = (state, action) => {
         const { newTodo } = action.payload
         state.push({ ...newTodo, completed: false })
@@ -124,9 +134,8 @@ describe('createReducer', () => {
         todo.completed = !todo.completed
       }
 
-      const todosReducer = createReducer([] as TodoState, {
-        ADD_TODO: addTodo,
-        TOGGLE_TODO: toggleTodo,
+      const todosReducer = createReducer([] as TodoState, (builder) => {
+        builder.addCase('ADD_TODO', addTodo).addCase('TOGGLE_TODO', toggleTodo)
       })
 
       const result = todosReducer([], {
@@ -142,7 +151,7 @@ describe('createReducer', () => {
 
     test('Freezes initial state', () => {
       const initialState = [{ text: 'Buy milk' }]
-      const todosReducer = createReducer(initialState, {})
+      const todosReducer = createReducer(initialState, () => {})
       const frozenInitialState = todosReducer(undefined, { type: 'dummy' })
 
       const mutateStateOutsideReducer = () =>
@@ -152,7 +161,9 @@ describe('createReducer', () => {
       )
     })
     test('does not throw error if initial state is not draftable', () => {
-      expect(() => createReducer(new URLSearchParams(), {})).not.toThrowError()
+      expect(() =>
+        createReducer(new URLSearchParams(), () => {})
+      ).not.toThrowError()
     })
   })
 
@@ -174,9 +185,8 @@ describe('createReducer', () => {
       })
     }
 
-    const todosReducer = createReducer([] as TodoState, {
-      ADD_TODO: addTodo,
-      TOGGLE_TODO: toggleTodo,
+    const todosReducer = createReducer([] as TodoState, (builder) => {
+      builder.addCase('ADD_TODO', addTodo).addCase('TOGGLE_TODO', toggleTodo)
     })
 
     behavesLikeReducer(todosReducer)
@@ -196,17 +206,16 @@ describe('createReducer', () => {
 
     const lazyStateInit = () => [] as TodoState
 
-    const todosReducer = createReducer(lazyStateInit, {
-      ADD_TODO: addTodo,
-      TOGGLE_TODO: toggleTodo,
+    const todosReducer = createReducer([] as TodoState, (builder) => {
+      builder.addCase('ADD_TODO', addTodo).addCase('TOGGLE_TODO', toggleTodo)
     })
 
     behavesLikeReducer(todosReducer)
 
     it('Should only call the init function when `undefined` state is passed in', () => {
-      const spy = jest.fn().mockReturnValue(42)
+      const spy = vi.fn().mockReturnValue(42)
 
-      const dummyReducer = createReducer(spy, {})
+      const dummyReducer = createReducer(spy, () => {})
       expect(spy).not.toHaveBeenCalled()
 
       dummyReducer(123, { type: 'dummy' })
@@ -233,9 +242,8 @@ describe('createReducer', () => {
       todo.completed = !todo.completed
     }
 
-    const todosReducer = createReducer([] as TodoState, {
-      ADD_TODO: addTodo,
-      TOGGLE_TODO: toggleTodo,
+    const todosReducer = createReducer([] as TodoState, (builder) => {
+      builder.addCase('ADD_TODO', addTodo).addCase('TOGGLE_TODO', toggleTodo)
     })
 
     const wrappedReducer: TodosReducer = (state = [], action) => {
@@ -247,121 +255,7 @@ 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', () => {
+  describe('builder callback for actionMap', () => {
     const increment = createAction<number, 'increment'>('increment')
     const decrement = createAction<number, 'decrement'>('decrement')
 
@@ -503,10 +397,19 @@ describe('createReducer', () => {
       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 numberActionMatcher = (
+      a: UnknownAction
+    ): a is PayloadAction<number> =>
+      isPlainObject(a.meta) &&
+      'type' in a.meta &&
+      (a.meta as Record<'type', unknown>).type === 'number_action'
+
+    const stringActionMatcher = (
+      a: UnknownAction
+    ): a is PayloadAction<string> =>
+      isPlainObject(a.meta) &&
+      'type' in a.meta &&
+      (a.meta as Record<'type', unknown>).type === 'string_action'
 
     const incrementBy = createAction('increment', prepareNumberAction)
     const decrementBy = createAction('decrement', prepareNumberAction)
diff --git a/packages/toolkit/src/tests/createReducer.typetest.ts b/packages/toolkit/src/tests/createReducer.typetest.ts
--- a/packages/toolkit/src/tests/createReducer.typetest.ts
+++ b/packages/toolkit/src/tests/createReducer.typetest.ts
@@ -7,16 +7,19 @@ import { expectType } from './helpers'
  * Test: createReducer() infers type of returned reducer.
  */
 {
-  type CounterAction =
-    | { type: 'increment'; payload: number }
-    | { type: 'decrement'; payload: number }
+  const incrementHandler = (
+    state: number,
+    action: { type: 'increment'; payload: number }
+  ) => state + 1
+  const decrementHandler = (
+    state: number,
+    action: { type: 'decrement'; payload: number }
+  ) => state - 1
 
-  const incrementHandler = (state: number, action: CounterAction) => state + 1
-  const decrementHandler = (state: number, action: CounterAction) => state - 1
-
-  const reducer = createReducer(0 as number, {
-    increment: incrementHandler,
-    decrement: decrementHandler,
+  const reducer = createReducer(0 as number, (builder) => {
+    builder
+      .addCase('increment', incrementHandler)
+      .addCase('decrement', decrementHandler)
   })
 
   const numberReducer: Reducer<number> = reducer
@@ -29,25 +32,28 @@ import { expectType } from './helpers'
  * Test: createReducer() state type can be specified expliclity.
  */
 {
-  type CounterAction =
-    | { type: 'increment'; payload: number }
-    | { type: 'decrement'; payload: number }
-
-  const incrementHandler = (state: number, action: CounterAction) =>
-    state + action.payload
+  const incrementHandler = (
+    state: number,
+    action: { type: 'increment'; payload: number }
+  ) => state + action.payload
 
-  const decrementHandler = (state: number, action: CounterAction) =>
-    state - action.payload
+  const decrementHandler = (
+    state: number,
+    action: { type: 'decrement'; payload: number }
+  ) => state - action.payload
 
-  createReducer<number>(0, {
-    increment: incrementHandler,
-    decrement: decrementHandler,
+  createReducer(0 as number, (builder) => {
+    builder
+      .addCase('increment', incrementHandler)
+      .addCase('decrement', decrementHandler)
   })
 
   // @ts-expect-error
-  createReducer<string>(0, {
-    increment: incrementHandler,
-    decrement: decrementHandler,
+  createReducer<string>(0 as number, (builder) => {
+    // @ts-expect-error
+    builder
+      .addCase('increment', incrementHandler)
+      .addCase('decrement', decrementHandler)
   })
 }
 
@@ -57,14 +63,14 @@ import { expectType } from './helpers'
 {
   const initialState: { readonly counter: number } = { counter: 0 }
 
-  createReducer(initialState, {
-    increment: (state) => {
+  createReducer(initialState, (builder) => {
+    builder.addCase('increment', (state) => {
       state.counter += 1
-    },
+    })
   })
 }
 
-/** Test:  alternative builder callback for actionMap */
+/** Test: builder callback for actionMap */
 {
   const increment = createAction<number, 'increment'>('increment')
 
diff --git a/packages/toolkit/src/tests/createSlice.test.ts b/packages/toolkit/src/tests/createSlice.test.ts
--- a/packages/toolkit/src/tests/createSlice.test.ts
+++ b/packages/toolkit/src/tests/createSlice.test.ts
@@ -1,5 +1,13 @@
-import type { PayloadAction } from '@reduxjs/toolkit'
-import { createSlice, createAction } from '@reduxjs/toolkit'
+import { vi } from 'vitest'
+import type { PayloadAction, WithSlice } from '@reduxjs/toolkit'
+import {
+  asyncThunkCreator,
+  buildCreateSlice,
+  configureStore,
+  combineSlices,
+  createSlice,
+  createAction,
+} from '@reduxjs/toolkit'
 import {
   mockConsole,
   createConsole,
@@ -183,9 +191,13 @@ describe('createSlice', () => {
         increment: (state) => state + 1,
         multiply: (state, action) => state * action.payload,
       },
-      extraReducers: {
-        [addMore.type]: (state, action) => state + action.payload.amount,
+      extraReducers: (builder) => {
+        builder.addCase(
+          addMore,
+          (state, action) => state + action.payload.amount
+        )
       },
+
       initialState: 0,
     })
 
@@ -195,7 +207,7 @@ describe('createSlice', () => {
       expect(result).toBe(15)
     })
 
-    describe('alternative builder callback for extraReducers', () => {
+    describe('builder callback for extraReducers', () => {
       const increment = createAction<number, 'increment'>('increment')
 
       test('can be used with actionCreators', () => {
@@ -240,7 +252,7 @@ describe('createSlice', () => {
           })
           slice.reducer(undefined, { type: 'unrelated' })
         }).toThrowErrorMatchingInlineSnapshot(
-          `"addCase cannot be called with two reducers for the same action type"`
+          '"`builder.addCase` cannot be called with two reducers for the same action type \'increment\'"'
         )
       })
 
@@ -265,7 +277,10 @@ describe('createSlice', () => {
           initialState: 0,
           reducers: {},
           extraReducers: (builder) =>
-            builder.addDefaultCase((state, action) => state + action.payload),
+            builder.addDefaultCase(
+              (state, action) =>
+                state + (action as PayloadAction<number>).payload
+            ),
         })
         expect(slice.reducer(0, increment(5))).toBe(5)
       })
@@ -276,7 +291,7 @@ describe('createSlice', () => {
 
   describe('behaviour with enhanced case reducers', () => {
     it('should pass all arguments to the prepare function', () => {
-      const prepare = jest.fn((payload, somethingElse) => ({ payload }))
+      const prepare = vi.fn((payload, somethingElse) => ({ payload }))
 
       const testSlice = createSlice({
         name: 'test',
@@ -297,7 +312,7 @@ describe('createSlice', () => {
     })
 
     it('should call the reducer function', () => {
-      const reducer = jest.fn(() => 5)
+      const reducer = vi.fn(() => 5)
 
       const testSlice = createSlice({
         name: 'test',
@@ -372,11 +387,11 @@ describe('createSlice', () => {
     })
   })
 
-  describe.only('Deprecation warnings', () => {
+  describe('Deprecation warnings', () => {
     let originalNodeEnv = process.env.NODE_ENV
 
     beforeEach(() => {
-      jest.resetModules()
+      vi.resetModules()
       restore = mockConsole(createConsole())
     })
 
@@ -385,40 +400,45 @@ describe('createSlice', () => {
     })
 
     // NOTE: This needs to be in front of the later `createReducer` call to check the one-time warning
-    it('Warns about object notation deprecation, once', () => {
-      const { createSlice } = require('../createSlice')
+    it('Throws an error if the legacy object notation is used', async () => {
+      const { createSlice } = await import('../createSlice')
 
       let dummySlice = (createSlice as CreateSlice)({
         name: 'dummy',
         initialState: [],
         reducers: {},
         extraReducers: {
+          // @ts-ignore
           a: () => [],
         },
       })
+      let reducer: any
       // Have to trigger the lazy creation
-      let { reducer } = dummySlice
-      reducer(undefined, { type: 'dummy' })
+      const wrapper = () => {
+        reducer = dummySlice.reducer
+        reducer(undefined, { type: 'dummy' })
+      }
 
-      expect(getLog().levels.warn).toMatch(
-        /The object notation for `createSlice.extraReducers` is deprecated/
+      expect(wrapper).toThrowError(
+        /The object notation for `createSlice.extraReducers` has been removed/
       )
-      restore = mockConsole(createConsole())
 
       dummySlice = (createSlice as CreateSlice)({
         name: 'dummy',
         initialState: [],
         reducers: {},
         extraReducers: {
+          // @ts-ignore
           a: () => [],
         },
       })
-      reducer = dummySlice.reducer
-      reducer(undefined, { type: 'dummy' })
-      expect(getLog().levels.warn).toBe('')
+      expect(wrapper).toThrowError(
+        /The object notation for `createSlice.extraReducers` has been removed/
+      )
     })
 
-    it('Does not warn in production', () => {
+    // TODO Determine final production behavior here
+    it.skip('Crashes in production', () => {
       process.env.NODE_ENV = 'production'
       const { createSlice } = require('../createSlice')
 
@@ -426,9 +446,388 @@ describe('createSlice', () => {
         name: 'dummy',
         initialState: [],
         reducers: {},
+        // @ts-ignore
         extraReducers: {},
       })
-      expect(getLog().levels.warn).toBe('')
+      const wrapper = () => {
+        let reducer = dummySlice.reducer
+        reducer(undefined, { type: 'dummy' })
+      }
+
+      expect(wrapper).toThrowError(
+        /The object notation for `createSlice.extraReducers` has been removed/
+      )
+    })
+  })
+  describe('slice selectors', () => {
+    const slice = createSlice({
+      name: 'counter',
+      initialState: 42,
+      reducers: {},
+      selectors: {
+        selectSlice: (state) => state,
+        selectMultiple: Object.assign(
+          (state: number, multiplier: number) => state * multiplier,
+          { test: 0 }
+        ),
+      },
+    })
+    it('expects reducer under slice.reducerPath if no selectState callback passed', () => {
+      const testState = {
+        [slice.reducerPath]: slice.getInitialState(),
+      }
+      const { selectSlice, selectMultiple } = slice.selectors
+      expect(selectSlice(testState)).toBe(slice.getInitialState())
+      expect(selectMultiple(testState, 2)).toBe(slice.getInitialState() * 2)
+    })
+    it('allows passing a selector for a custom location', () => {
+      const customState = {
+        number: slice.getInitialState(),
+      }
+      const { selectSlice, selectMultiple } = slice.getSelectors(
+        (state: typeof customState) => state.number
+      )
+      expect(selectSlice(customState)).toBe(slice.getInitialState())
+      expect(selectMultiple(customState, 2)).toBe(slice.getInitialState() * 2)
+    })
+    it('allows accessing properties on the selector', () => {
+      expect(slice.selectors.selectMultiple.unwrapped.test).toBe(0)
+    })
+  })
+  describe('slice injections', () => {
+    it('uses injectInto to inject slice into combined reducer', () => {
+      const slice = createSlice({
+        name: 'counter',
+        initialState: 42,
+        reducers: {
+          increment: (state) => ++state,
+        },
+        selectors: {
+          selectMultiple: (state, multiplier: number) => state * multiplier,
+        },
+      })
+
+      const { increment } = slice.actions
+
+      const combinedReducer = combineSlices({
+        static: slice.reducer,
+      }).withLazyLoadedSlices<WithSlice<typeof slice>>()
+
+      const uninjectedState = combinedReducer(undefined, increment())
+
+      expect(uninjectedState.counter).toBe(undefined)
+
+      const injectedSlice = slice.injectInto(combinedReducer)
+
+      // selector returns initial state if undefined in real state
+      expect(injectedSlice.selectSlice(uninjectedState)).toBe(
+        slice.getInitialState()
+      )
+
+      const injectedState = combinedReducer(undefined, increment())
+
+      expect(injectedSlice.selectSlice(injectedState)).toBe(
+        slice.getInitialState() + 1
+      )
+    })
+    it('allows providing a custom name to inject under', () => {
+      const slice = createSlice({
+        name: 'counter',
+        reducerPath: 'injected',
+        initialState: 42,
+        reducers: {
+          increment: (state) => ++state,
+        },
+        selectors: {
+          selectMultiple: (state, multiplier: number) => state * multiplier,
+        },
+      })
+
+      const { increment } = slice.actions
+
+      const combinedReducer = combineSlices({
+        static: slice.reducer,
+      }).withLazyLoadedSlices<WithSlice<typeof slice> & { injected2: number }>()
+
+      const uninjectedState = combinedReducer(undefined, increment())
+
+      expect(uninjectedState.injected).toBe(undefined)
+
+      const injected = slice.injectInto(combinedReducer)
+
+      const injectedState = combinedReducer(undefined, increment())
+
+      expect(injected.selectSlice(injectedState)).toBe(
+        slice.getInitialState() + 1
+      )
+      expect(injected.selectors.selectMultiple(injectedState, 2)).toBe(
+        (slice.getInitialState() + 1) * 2
+      )
+
+      const injected2 = slice.injectInto(combinedReducer, {
+        reducerPath: 'injected2',
+      })
+
+      const injected2State = combinedReducer(undefined, increment())
+
+      expect(injected2.selectSlice(injected2State)).toBe(
+        slice.getInitialState() + 1
+      )
+      expect(injected2.selectors.selectMultiple(injected2State, 2)).toBe(
+        (slice.getInitialState() + 1) * 2
+      )
+    })
+  })
+  describe('reducers definition with asyncThunks', () => {
+    it('is disabled by default', () => {
+      expect(() =>
+        createSlice({
+          name: 'test',
+          initialState: [] as any[],
+          reducers: (create) => ({ thunk: create.asyncThunk(() => {}) }),
+        })
+      ).toThrowErrorMatchingInlineSnapshot(
+        '"Cannot use `create.asyncThunk` in the built-in `createSlice`. Use `buildCreateSlice({ creators: { asyncThunk: asyncThunkCreator } })` to create a customised version of `createSlice`."'
+      )
+    })
+    const createThunkSlice = buildCreateSlice({
+      creators: { asyncThunk: asyncThunkCreator },
+    })
+    function pending(state: any[], action: any) {
+      state.push(['pendingReducer', action])
+    }
+    function fulfilled(state: any[], action: any) {
+      state.push(['fulfilledReducer', action])
+    }
+    function rejected(state: any[], action: any) {
+      state.push(['rejectedReducer', action])
+    }
+    function settled(state: any[], action: any) {
+      state.push(['settledReducer', action])
+    }
+
+    test('successful thunk', async () => {
+      const slice = createThunkSlice({
+        name: 'test',
+        initialState: [] as any[],
+        reducers: (create) => ({
+          thunkReducers: create.asyncThunk(
+            function payloadCreator(arg, api) {
+              return Promise.resolve('resolved payload')
+            },
+            { pending, fulfilled, rejected, settled }
+          ),
+        }),
+      })
+
+      const store = configureStore({
+        reducer: slice.reducer,
+      })
+      await store.dispatch(slice.actions.thunkReducers('test'))
+      expect(store.getState()).toMatchObject([
+        [
+          'pendingReducer',
+          {
+            type: 'test/thunkReducers/pending',
+            payload: undefined,
+          },
+        ],
+        [
+          'fulfilledReducer',
+          {
+            type: 'test/thunkReducers/fulfilled',
+            payload: 'resolved payload',
+          },
+        ],
+        [
+          'settledReducer',
+          {
+            type: 'test/thunkReducers/fulfilled',
+            payload: 'resolved payload',
+          },
+        ],
+      ])
+    })
+
+    test('rejected thunk', async () => {
+      const slice = createThunkSlice({
+        name: 'test',
+        initialState: [] as any[],
+        reducers: (create) => ({
+          thunkReducers: create.asyncThunk(
+            // payloadCreator isn't allowed to return never
+            function payloadCreator(arg, api): any {
+              throw new Error('')
+            },
+            { pending, fulfilled, rejected, settled }
+          ),
+        }),
+      })
+
+      const store = configureStore({
+        reducer: slice.reducer,
+      })
+      await store.dispatch(slice.actions.thunkReducers('test'))
+      expect(store.getState()).toMatchObject([
+        [
+          'pendingReducer',
+          {
+            type: 'test/thunkReducers/pending',
+            payload: undefined,
+          },
+        ],
+        [
+          'rejectedReducer',
+          {
+            type: 'test/thunkReducers/rejected',
+            payload: undefined,
+          },
+        ],
+        [
+          'settledReducer',
+          {
+            type: 'test/thunkReducers/rejected',
+            payload: undefined,
+          },
+        ],
+      ])
+    })
+
+    test('with options', async () => {
+      const slice = createThunkSlice({
+        name: 'test',
+        initialState: [] as any[],
+        reducers: (create) => ({
+          thunkReducers: create.asyncThunk(
+            function payloadCreator(arg, api) {
+              return 'should not call this'
+            },
+            {
+              options: {
+                condition() {
+                  return false
+                },
+                dispatchConditionRejection: true,
+              },
+              pending,
+              fulfilled,
+              rejected,
+              settled,
+            }
+          ),
+        }),
+      })
+
+      const store = configureStore({
+        reducer: slice.reducer,
+      })
+      await store.dispatch(slice.actions.thunkReducers('test'))
+      expect(store.getState()).toMatchObject([
+        [
+          'rejectedReducer',
+          {
+            type: 'test/thunkReducers/rejected',
+            payload: undefined,
+            meta: { condition: true },
+          },
+        ],
+        [
+          'settledReducer',
+          {
+            type: 'test/thunkReducers/rejected',
+            payload: undefined,
+            meta: { condition: true },
+          },
+        ],
+      ])
+    })
+
+    test('has caseReducers for the asyncThunk', async () => {
+      const slice = createThunkSlice({
+        name: 'test',
+        initialState: [],
+        reducers: (create) => ({
+          thunkReducers: create.asyncThunk(
+            function payloadCreator(arg, api) {
+              return Promise.resolve('resolved payload')
+            },
+            { pending, fulfilled, settled }
+          ),
+        }),
+      })
+
+      expect(slice.caseReducers.thunkReducers.pending).toBe(pending)
+      expect(slice.caseReducers.thunkReducers.fulfilled).toBe(fulfilled)
+      expect(slice.caseReducers.thunkReducers.settled).toBe(settled)
+      // even though it is not defined above, this should at least be a no-op function to match the TypeScript typings
+      // and should be callable as a reducer even if it does nothing
+      expect(() =>
+        slice.caseReducers.thunkReducers.rejected(
+          [],
+          slice.actions.thunkReducers.rejected(
+            new Error('test'),
+            'fakeRequestId',
+            {}
+          )
+        )
+      ).not.toThrow()
+    })
+
+    test('can define reducer with prepare statement using create.preparedReducer', async () => {
+      const slice = createSlice({
+        name: 'test',
+        initialState: [] as any[],
+        reducers: (create) => ({
+          prepared: create.preparedReducer(
+            (p: string, m: number, e: { message: string }) => ({
+              payload: p,
+              meta: m,
+              error: e,
+            }),
+            (state, action) => {
+              state.push(action)
+            }
+          ),
+        }),
+      })
+
+      expect(
+        slice.reducer([], slice.actions.prepared('test', 1, { message: 'err' }))
+      ).toMatchInlineSnapshot(`
+        [
+          {
+            "error": {
+              "message": "err",
+            },
+            "meta": 1,
+            "payload": "test",
+            "type": "test/prepared",
+          },
+        ]
+      `)
+    })
+
+    test('throws an error when invoked with a normal `prepare` object that has not gone through a `create.preparedReducer` call', async () => {
+      expect(() =>
+        createSlice({
+          name: 'test',
+          initialState: [] as any[],
+          reducers: (create) => ({
+            prepared: {
+              prepare: (p: string, m: number, e: { message: string }) => ({
+                payload: p,
+                meta: m,
+                error: e,
+              }),
+              reducer: (state, action) => {
+                state.push(action)
+              },
+            },
+          }),
+        })
+      ).toThrowErrorMatchingInlineSnapshot(
+        `"Please use the \`create.preparedReducer\` notation for prepared action creators with the \`create\` notation."`
+      )
     })
   })
 })
diff --git a/packages/toolkit/src/tests/createSlice.typetest.ts b/packages/toolkit/src/tests/createSlice.typetest.ts
--- a/packages/toolkit/src/tests/createSlice.typetest.ts
+++ b/packages/toolkit/src/tests/createSlice.typetest.ts
@@ -1,4 +1,4 @@
-import type { Action, AnyAction, Reducer } from 'redux'
+import type { Action, UnknownAction, Reducer } from 'redux'
 import type {
   ActionCreatorWithNonInferrablePayload,
   ActionCreatorWithOptionalPayload,
@@ -6,12 +6,27 @@ import type {
   ActionCreatorWithPayload,
   ActionCreatorWithPreparedPayload,
   ActionReducerMapBuilder,
+  AsyncThunk,
+  CaseReducer,
   PayloadAction,
+  PayloadActionCreator,
+  ReducerCreators,
+  SerializedError,
   SliceCaseReducers,
+  ThunkDispatch,
   ValidateSliceCaseReducers,
 } from '@reduxjs/toolkit'
-import { createAction, createSlice } from '@reduxjs/toolkit'
-import { expectType } from './helpers'
+import {
+  configureStore,
+  isRejected,
+  createAction,
+  createSlice,
+  buildCreateSlice,
+  asyncThunkCreator,
+  createAsyncThunk,
+} from '@reduxjs/toolkit'
+import { expectExactType, expectType, expectUnknown } from './helpers'
+import { castDraft } from 'immer'
 
 /*
  * Test: Slice name is strongly typed.
@@ -74,7 +89,7 @@ const value = actionCreators.anyKey
   // @ts-expect-error
   expectType<Reducer<string, PayloadAction>>(slice.reducer)
   // @ts-expect-error
-  expectType<Reducer<string, AnyAction>>(slice.reducer)
+  expectType<Reducer<string, UnknownAction>>(slice.reducer)
   /* Actions */
 
   slice.actions.increment(1)
@@ -386,19 +401,19 @@ const value = actionCreators.anyKey
     },
   })
 
-  const x: Action<unknown> = {} as any
+  const x: Action<string> = {} as any
   if (mySlice.actions.setName.match(x)) {
     expectType<'name/setName'>(x.type)
     expectType<string>(x.payload)
   } else {
     // @ts-expect-error
-    expectType<string>(x.type)
+    expectType<'name/setName'>(x.type)
     // @ts-expect-error
     expectType<string>(x.payload)
   }
 }
 
-/** Test:  alternative builder callback for extraReducers */
+/** Test:  builder callback for extraReducers */
 {
   createSlice({
     name: 'test',
@@ -499,3 +514,362 @@ const value = actionCreators.anyKey
     return { doNothing, setData, slice }
   }
 }
+
+/**
+ * Test: slice selectors
+ */
+
+{
+  const sliceWithoutSelectors = createSlice({
+    name: '',
+    initialState: '',
+    reducers: {},
+  })
+
+  // @ts-expect-error
+  sliceWithoutSelectors.selectors.foo
+
+  const sliceWithSelectors = createSlice({
+    name: 'counter',
+    initialState: { value: 0 },
+    reducers: {
+      increment: (state) => {
+        state.value += 1
+      },
+    },
+    selectors: {
+      selectValue: (state) => state.value,
+      selectMultiply: (state, multiplier: number) => state.value * multiplier,
+      selectToFixed: Object.assign(
+        (state: { value: number }) => state.value.toFixed(2),
+        { static: true }
+      ),
+    },
+  })
+
+  const rootState = {
+    [sliceWithSelectors.reducerPath]: sliceWithSelectors.getInitialState(),
+  }
+
+  const { selectValue, selectMultiply, selectToFixed } =
+    sliceWithSelectors.selectors
+
+  expectType<number>(selectValue(rootState))
+  expectType<number>(selectMultiply(rootState, 2))
+  expectType<string>(selectToFixed(rootState))
+
+  expectType<boolean>(selectToFixed.unwrapped.static)
+
+  const nestedState = {
+    nested: rootState,
+  }
+
+  const nestedSelectors = sliceWithSelectors.getSelectors(
+    (rootState: typeof nestedState) => rootState.nested.counter
+  )
+
+  expectType<number>(nestedSelectors.selectValue(nestedState))
+  expectType<number>(nestedSelectors.selectMultiply(nestedState, 2))
+  expectType<string>(nestedSelectors.selectToFixed(nestedState))
+}
+
+/**
+ * Test: reducer callback
+ */
+
+{
+  interface TestState {
+    foo: string
+  }
+
+  interface TestArg {
+    test: string
+  }
+
+  interface TestReturned {
+    payload: string
+  }
+
+  interface TestReject {
+    cause: string
+  }
+
+  const slice = createSlice({
+    name: 'test',
+    initialState: {} as TestState,
+    reducers: (create) => {
+      const pretypedAsyncThunk =
+        create.asyncThunk.withTypes<{ rejectValue: TestReject }>()
+
+      // @ts-expect-error
+      create.asyncThunk<any, any, { state: StoreState }>(() => {})
+
+      // @ts-expect-error
+      create.asyncThunk.withTypes<{
+        rejectValue: string
+        dispatch: StoreDispatch
+      }>()
+
+      return {
+        normalReducer: create.reducer<string>((state, action) => {
+          expectType<TestState>(state)
+          expectType<string>(action.payload)
+        }),
+        optionalReducer: create.reducer<string | undefined>((state, action) => {
+          expectType<TestState>(state)
+          expectType<string | undefined>(action.payload)
+        }),
+        noActionReducer: create.reducer((state) => {
+          expectType<TestState>(state)
+        }),
+        preparedReducer: create.preparedReducer(
+          (payload: string) => ({
+            payload,
+            meta: 'meta' as const,
+            error: 'error' as const,
+          }),
+          (state, action) => {
+            expectType<TestState>(state)
+            expectType<string>(action.payload)
+            expectExactType('meta' as const)(action.meta)
+            expectExactType('error' as const)(action.error)
+          }
+        ),
+        testInfer: create.asyncThunk(
+          function payloadCreator(arg: TestArg, api) {
+            return Promise.resolve<TestReturned>({ payload: 'foo' })
+          },
+          {
+            pending(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+            },
+            fulfilled(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+              expectType<TestReturned>(action.payload)
+            },
+            rejected(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+              expectType<SerializedError>(action.error)
+            },
+            settled(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+              if (isRejected(action)) {
+                expectType<SerializedError>(action.error)
+              } else {
+                expectType<TestReturned>(action.payload)
+              }
+            },
+          }
+        ),
+        testExplicitType: create.asyncThunk<
+          TestArg,
+          TestReturned,
+          {
+            rejectValue: TestReject
+          }
+        >(
+          function payloadCreator(arg, api) {
+            // here would be a circular reference
+            expectUnknown(api.getState())
+            // here would be a circular reference
+            expectType<ThunkDispatch<any, any, any>>(api.dispatch)
+            // so you need to cast inside instead
+            const getState = api.getState as () => StoreState
+            const dispatch = api.dispatch as StoreDispatch
+            expectType<TestArg>(arg)
+            expectType<(value: TestReject) => any>(api.rejectWithValue)
+            return Promise.resolve({ payload: 'foo' })
+          },
+          {
+            pending(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+            },
+            fulfilled(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+              expectType<TestReturned>(action.payload)
+            },
+            rejected(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+              expectType<SerializedError>(action.error)
+              expectType<TestReject | undefined>(action.payload)
+            },
+            settled(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+              if (isRejected(action)) {
+                expectType<SerializedError>(action.error)
+                expectType<TestReject | undefined>(action.payload)
+              } else {
+                expectType<TestReturned>(action.payload)
+              }
+            },
+          }
+        ),
+        testPretyped: pretypedAsyncThunk(
+          function payloadCreator(arg: TestArg, api) {
+            expectType<(value: TestReject) => any>(api.rejectWithValue)
+            return Promise.resolve<TestReturned>({ payload: 'foo' })
+          },
+          {
+            pending(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+            },
+            fulfilled(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+              expectType<TestReturned>(action.payload)
+            },
+            rejected(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+              expectType<SerializedError>(action.error)
+              expectType<TestReject | undefined>(action.payload)
+            },
+            settled(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+              if (isRejected(action)) {
+                expectType<SerializedError>(action.error)
+                expectType<TestReject | undefined>(action.payload)
+              } else {
+                expectType<TestReturned>(action.payload)
+              }
+            },
+          }
+        ),
+      }
+    },
+  })
+
+  const store = configureStore({ reducer: { test: slice.reducer } })
+
+  type StoreState = ReturnType<typeof store.getState>
+  type StoreDispatch = typeof store.dispatch
+
+  expectType<PayloadActionCreator<string>>(slice.actions.normalReducer)
+  slice.actions.normalReducer('')
+  // @ts-expect-error
+  slice.actions.normalReducer()
+  // @ts-expect-error
+  slice.actions.normalReducer(0)
+  expectType<ActionCreatorWithOptionalPayload<string | undefined>>(
+    slice.actions.optionalReducer
+  )
+  slice.actions.optionalReducer()
+  slice.actions.optionalReducer('')
+  // @ts-expect-error
+  slice.actions.optionalReducer(0)
+
+  expectType<ActionCreatorWithoutPayload>(slice.actions.noActionReducer)
+  slice.actions.noActionReducer()
+  // @ts-expect-error
+  slice.actions.noActionReducer('')
+  expectType<
+    ActionCreatorWithPreparedPayload<
+      [string],
+      string,
+      'test/preparedReducer',
+      'error',
+      'meta'
+    >
+  >(slice.actions.preparedReducer)
+  expectType<AsyncThunk<TestReturned, TestArg, {}>>(slice.actions.testInfer)
+  expectType<AsyncThunk<TestReturned, TestArg, { rejectValue: TestReject }>>(
+    slice.actions.testExplicitType
+  )
+  {
+    type TestInferThunk = AsyncThunk<TestReturned, TestArg, {}>
+    expectType<CaseReducer<TestState, ReturnType<TestInferThunk['pending']>>>(
+      slice.caseReducers.testInfer.pending
+    )
+    expectType<CaseReducer<TestState, ReturnType<TestInferThunk['fulfilled']>>>(
+      slice.caseReducers.testInfer.fulfilled
+    )
+    expectType<CaseReducer<TestState, ReturnType<TestInferThunk['rejected']>>>(
+      slice.caseReducers.testInfer.rejected
+    )
+  }
+}
+
+/** Test: wrapping createSlice should be possible, with callback */
+{
+  interface GenericState<T> {
+    data?: T
+    status: 'loading' | 'finished' | 'error'
+  }
+
+  const createGenericSlice = <
+    T,
+    Reducers extends SliceCaseReducers<GenericState<T>>
+  >({
+    name = '',
+    initialState,
+    reducers,
+  }: {
+    name: string
+    initialState: GenericState<T>
+    reducers: (create: ReducerCreators<GenericState<T>>) => Reducers
+  }) => {
+    return createSlice({
+      name,
+      initialState,
+      reducers: (create) => ({
+        start: create.reducer((state) => {
+          state.status = 'loading'
+        }),
+        success: create.reducer<T>((state, action) => {
+          state.data = castDraft(action.payload)
+          state.status = 'finished'
+        }),
+        ...reducers(create),
+      }),
+    })
+  }
+
+  const wrappedSlice = createGenericSlice({
+    name: 'test',
+    initialState: { status: 'loading' } as GenericState<string>,
+    reducers: (create) => ({
+      magic: create.reducer((state) => {
+        expectType<GenericState<string>>(state)
+        // @ts-expect-error
+        expectType<GenericState<number>>(state)
+
+        state.status = 'finished'
+        state.data = 'hocus pocus'
+      }),
+    }),
+  })
+
+  expectType<ActionCreatorWithPayload<string>>(wrappedSlice.actions.success)
+  expectType<ActionCreatorWithoutPayload<string>>(wrappedSlice.actions.magic)
+}
+
+/**
+ * Test: selectSlice
+ */
+{
+  expectType<number>(counterSlice.selectSlice({ counter: 0 }))
+  // @ts-expect-error
+  counterSlice.selectSlice({})
+}
+
+/**
+ * Test: buildCreateSlice
+ */
+{
+  expectExactType(createSlice)(buildCreateSlice())
+  buildCreateSlice({
+    // @ts-expect-error not possible to recreate shape because symbol is not exported
+    creators: { asyncThunk: { [Symbol()]: createAsyncThunk } },
+  })
+  buildCreateSlice({ creators: { asyncThunk: asyncThunkCreator } })
+}
diff --git a/packages/toolkit/src/tests/EnhancerArray.typetest.ts b/packages/toolkit/src/tests/getDefaultEnhancers.typetest.ts
similarity index 86%
rename from packages/toolkit/src/tests/EnhancerArray.typetest.ts
rename to packages/toolkit/src/tests/getDefaultEnhancers.typetest.ts
--- a/packages/toolkit/src/tests/EnhancerArray.typetest.ts
+++ b/packages/toolkit/src/tests/getDefaultEnhancers.typetest.ts
@@ -22,7 +22,7 @@ declare const enhancer2: StoreEnhancer<
   {
     const store = configureStore({
       reducer: () => 0,
-      enhancers: (dE) => dE.prepend(enhancer1),
+      enhancers: (gDE) => gDE().prepend(enhancer1),
     })
     expectType<true>(store.has1)
     expectType<true>(store.getState().stateHas1)
@@ -37,7 +37,7 @@ declare const enhancer2: StoreEnhancer<
   {
     const store = configureStore({
       reducer: () => 0,
-      enhancers: (dE) => dE.prepend(enhancer1, enhancer2),
+      enhancers: (gDE) => gDE().prepend(enhancer1, enhancer2),
     })
     expectType<true>(store.has1)
     expectType<true>(store.getState().stateHas1)
@@ -54,7 +54,7 @@ declare const enhancer2: StoreEnhancer<
   {
     const store = configureStore({
       reducer: () => 0,
-      enhancers: (dE) => dE.prepend([enhancer1, enhancer2] as const),
+      enhancers: (gDE) => gDE().prepend([enhancer1, enhancer2] as const),
     })
     expectType<true>(store.has1)
     expectType<true>(store.getState().stateHas1)
@@ -71,7 +71,7 @@ declare const enhancer2: StoreEnhancer<
   {
     const store = configureStore({
       reducer: () => 0,
-      enhancers: (dE) => dE.concat(enhancer1),
+      enhancers: (gDE) => gDE().concat(enhancer1),
     })
     expectType<true>(store.has1)
     expectType<true>(store.getState().stateHas1)
@@ -86,7 +86,7 @@ declare const enhancer2: StoreEnhancer<
   {
     const store = configureStore({
       reducer: () => 0,
-      enhancers: (dE) => dE.concat(enhancer1, enhancer2),
+      enhancers: (gDE) => gDE().concat(enhancer1, enhancer2),
     })
     expectType<true>(store.has1)
     expectType<true>(store.getState().stateHas1)
@@ -103,7 +103,7 @@ declare const enhancer2: StoreEnhancer<
   {
     const store = configureStore({
       reducer: () => 0,
-      enhancers: (dE) => dE.concat([enhancer1, enhancer2] as const),
+      enhancers: (gDE) => gDE().concat([enhancer1, enhancer2] as const),
     })
     expectType<true>(store.has1)
     expectType<true>(store.getState().stateHas1)
@@ -120,7 +120,7 @@ declare const enhancer2: StoreEnhancer<
   {
     const store = configureStore({
       reducer: () => 0,
-      enhancers: (dE) => dE.concat(enhancer1).prepend(enhancer2),
+      enhancers: (gDE) => gDE().concat(enhancer1).prepend(enhancer2),
     })
     expectType<true>(store.has1)
     expectType<true>(store.getState().stateHas1)
diff --git a/packages/toolkit/src/tests/getDefaultMiddleware.test.ts b/packages/toolkit/src/tests/getDefaultMiddleware.test.ts
--- a/packages/toolkit/src/tests/getDefaultMiddleware.test.ts
+++ b/packages/toolkit/src/tests/getDefaultMiddleware.test.ts
@@ -1,21 +1,22 @@
+import { vi } from 'vitest'
 import type {
-  AnyAction,
+  UnknownAction,
   Middleware,
   ThunkAction,
   Action,
   ThunkDispatch,
   Dispatch,
 } from '@reduxjs/toolkit'
-import {
-  getDefaultMiddleware,
-  MiddlewareArray,
-  configureStore,
-} from '@reduxjs/toolkit'
-import thunk from 'redux-thunk'
+import { configureStore } from '@reduxjs/toolkit'
+import { thunk } from 'redux-thunk'
 import type { ThunkMiddleware } from 'redux-thunk'
 
 import { expectType } from './helpers'
-import { BaseActionCreator } from '@internal/createAction'
+
+import { buildGetDefaultMiddleware } from '@internal/getDefaultMiddleware'
+import { Tuple } from '@internal/utils'
+
+const getDefaultMiddleware = buildGetDefaultMiddleware()
 
 describe('getDefaultMiddleware', () => {
   const ORIGINAL_NODE_ENV = process.env.NODE_ENV
@@ -24,10 +25,22 @@ describe('getDefaultMiddleware', () => {
     process.env.NODE_ENV = ORIGINAL_NODE_ENV
   })
 
-  it('returns an array with only redux-thunk in production', () => {
-    process.env.NODE_ENV = 'production'
+  describe('Production behavior', () => {
+    beforeEach(() => {
+      vi.resetModules()
+    })
+
+    it('returns an array with only redux-thunk in production', async () => {
+      process.env.NODE_ENV = 'production'
+      const { thunk } = await import('redux-thunk')
+      const { buildGetDefaultMiddleware } = await import(
+        '@internal/getDefaultMiddleware'
+      )
 
-    expect(getDefaultMiddleware()).toEqual([thunk]) // @remap-prod-remove-line
+      const middleware = buildGetDefaultMiddleware()()
+      expect(middleware).toContain(thunk)
+      expect(middleware.length).toBe(1)
+    })
   })
 
   it('returns an array with additional middleware in development', () => {
@@ -62,62 +75,69 @@ describe('getDefaultMiddleware', () => {
 
   it('allows passing options to thunk', () => {
     const extraArgument = 42 as const
-    const middleware = getDefaultMiddleware({
-      thunk: { extraArgument },
-      immutableCheck: false,
-      serializableCheck: false,
-      actionCreatorCheck: false,
-    })
 
     const m2 = getDefaultMiddleware({
       thunk: false,
     })
 
-    expectType<MiddlewareArray<[]>>(m2)
+    expectType<Tuple<[]>>(m2)
 
     const dummyMiddleware: Middleware<
       {
         (action: Action<'actionListenerMiddleware/add'>): () => void
       },
       { counter: number }
-    > = (storeApi) => (next) => (action) => {}
-
-    const dummyMiddleware2: Middleware = (storeApi) => (next) => (action) => {}
-
-    const m3 = middleware.concat(dummyMiddleware, dummyMiddleware2)
-
-    expectType<
-      MiddlewareArray<
-        [
-          ThunkMiddleware<any, AnyAction, 42>,
-          Middleware<
-            (action: Action<'actionListenerMiddleware/add'>) => () => void,
-            {
-              counter: number
-            },
-            Dispatch<AnyAction>
-          >,
-          Middleware<{}, any, Dispatch<AnyAction>>
-        ]
-      >
-    >(m3)
-
-    const testThunk: ThunkAction<void, {}, number, AnyAction> = (
-      dispatch,
-      getState,
-      extraArg
-    ) => {
+    > = (storeApi) => (next) => (action) => {
+      return next(action)
+    }
+
+    const dummyMiddleware2: Middleware<{}, { counter: number }> =
+      (storeApi) => (next) => (action) => {}
+
+    const testThunk: ThunkAction<
+      void,
+      { counter: number },
+      number,
+      UnknownAction
+    > = (dispatch, getState, extraArg) => {
       expect(extraArg).toBe(extraArgument)
     }
 
-    const reducer = () => ({})
+    const reducer = () => ({ counter: 123 })
 
     const store = configureStore({
       reducer,
-      middleware,
+      middleware: (gDM) => {
+        const middleware = gDM({
+          thunk: { extraArgument },
+          immutableCheck: false,
+          serializableCheck: false,
+          actionCreatorCheck: false,
+        })
+
+        const m3 = middleware.concat(dummyMiddleware, dummyMiddleware2)
+
+        expectType<
+          Tuple<
+            [
+              ThunkMiddleware<any, UnknownAction, 42>,
+              Middleware<
+                (action: Action<'actionListenerMiddleware/add'>) => () => void,
+                {
+                  counter: number
+                },
+                Dispatch<UnknownAction>
+              >,
+              Middleware<{}, any, Dispatch<UnknownAction>>
+            ]
+          >
+        >(m3)
+
+        return m3
+      },
     })
 
-    expectType<ThunkDispatch<any, 42, AnyAction> & Dispatch<AnyAction>>(
+    expectType<ThunkDispatch<any, 42, UnknownAction> & Dispatch<UnknownAction>>(
       store.dispatch
     )
 
@@ -127,17 +147,18 @@ describe('getDefaultMiddleware', () => {
   it('allows passing options to immutableCheck', () => {
     let immutableCheckWasCalled = false
 
-    const middleware = getDefaultMiddleware({
-      thunk: false,
-      immutableCheck: {
-        isImmutable: () => {
-          immutableCheckWasCalled = true
-          return true
+    const middleware = () =>
+      getDefaultMiddleware({
+        thunk: false,
+        immutableCheck: {
+          isImmutable: () => {
+            immutableCheckWasCalled = true
+            return true
+          },
         },
-      },
-      serializableCheck: false,
-      actionCreatorCheck: false,
-    })
+        serializableCheck: false,
+        actionCreatorCheck: false,
+      })
 
     const reducer = () => ({})
 
@@ -152,17 +173,18 @@ describe('getDefaultMiddleware', () => {
   it('allows passing options to serializableCheck', () => {
     let serializableCheckWasCalled = false
 
-    const middleware = getDefaultMiddleware({
-      thunk: false,
-      immutableCheck: false,
-      serializableCheck: {
-        isSerializable: () => {
-          serializableCheckWasCalled = true
-          return true
+    const middleware = () =>
+      getDefaultMiddleware({
+        thunk: false,
+        immutableCheck: false,
+        serializableCheck: {
+          isSerializable: () => {
+            serializableCheckWasCalled = true
+            return true
+          },
         },
-      },
-      actionCreatorCheck: false,
-    })
+        actionCreatorCheck: false,
+      })
 
     const reducer = () => ({})
 
@@ -180,17 +202,18 @@ describe('getDefaultMiddleware', () => {
 it('allows passing options to actionCreatorCheck', () => {
   let actionCreatorCheckWasCalled = false
 
-  const middleware = getDefaultMiddleware({
-    thunk: false,
-    immutableCheck: false,
-    serializableCheck: false,
-    actionCreatorCheck: {
-      isActionCreator: (action: unknown): action is Function => {
-        actionCreatorCheckWasCalled = true
-        return false
+  const middleware = () =>
+    getDefaultMiddleware({
+      thunk: false,
+      immutableCheck: false,
+      serializableCheck: false,
+      actionCreatorCheck: {
+        isActionCreator: (action: unknown): action is Function => {
+          actionCreatorCheckWasCalled = true
+          return false
+        },
       },
-    },
-  })
+    })
 
   const reducer = () => ({})
 
@@ -204,7 +227,7 @@ it('allows passing options to actionCreatorCheck', () => {
   expect(actionCreatorCheckWasCalled).toBe(true)
 })
 
-describe('MiddlewareArray functionality', () => {
+describe('Tuple functionality', () => {
   const middleware1: Middleware = () => (next) => (action) => next(action)
   const middleware2: Middleware = () => (next) => (action) => next(action)
   const defaultMiddleware = getDefaultMiddleware()
@@ -216,7 +239,7 @@ describe('MiddlewareArray functionality', () => {
     // value is prepended
     expect(prepended).toEqual([middleware1, ...defaultMiddleware])
     // returned value is of correct type
-    expect(prepended).toBeInstanceOf(MiddlewareArray)
+    expect(prepended).toBeInstanceOf(Tuple)
     // prepended is a new array
     expect(prepended).not.toEqual(defaultMiddleware)
     // defaultMiddleware is not modified
@@ -229,7 +252,7 @@ describe('MiddlewareArray functionality', () => {
     // value is prepended
     expect(prepended).toEqual([middleware1, middleware2, ...defaultMiddleware])
     // returned value is of correct type
-    expect(prepended).toBeInstanceOf(MiddlewareArray)
+    expect(prepended).toBeInstanceOf(Tuple)
     // prepended is a new array
     expect(prepended).not.toEqual(defaultMiddleware)
     // defaultMiddleware is not modified
@@ -242,7 +265,7 @@ describe('MiddlewareArray functionality', () => {
     // value is prepended
     expect(prepended).toEqual([middleware1, middleware2, ...defaultMiddleware])
     // returned value is of correct type
-    expect(prepended).toBeInstanceOf(MiddlewareArray)
+    expect(prepended).toBeInstanceOf(Tuple)
     // prepended is a new array
     expect(prepended).not.toEqual(defaultMiddleware)
     // defaultMiddleware is not modified
@@ -255,7 +278,7 @@ describe('MiddlewareArray functionality', () => {
     // value is concatenated
     expect(concatenated).toEqual([...defaultMiddleware, middleware1])
     // returned value is of correct type
-    expect(concatenated).toBeInstanceOf(MiddlewareArray)
+    expect(concatenated).toBeInstanceOf(Tuple)
     // concatenated is a new array
     expect(concatenated).not.toEqual(defaultMiddleware)
     // defaultMiddleware is not modified
@@ -272,7 +295,7 @@ describe('MiddlewareArray functionality', () => {
       middleware2,
     ])
     // returned value is of correct type
-    expect(concatenated).toBeInstanceOf(MiddlewareArray)
+    expect(concatenated).toBeInstanceOf(Tuple)
     // concatenated is a new array
     expect(concatenated).not.toEqual(defaultMiddleware)
     // defaultMiddleware is not modified
@@ -289,7 +312,7 @@ describe('MiddlewareArray functionality', () => {
       middleware2,
     ])
     // returned value is of correct type
-    expect(concatenated).toBeInstanceOf(MiddlewareArray)
+    expect(concatenated).toBeInstanceOf(Tuple)
     // concatenated is a new array
     expect(concatenated).not.toEqual(defaultMiddleware)
     // defaultMiddleware is not modified
diff --git a/packages/toolkit/src/tests/MiddlewareArray.typetest.ts b/packages/toolkit/src/tests/getDefaultMiddleware.typetest.ts
similarity index 97%
rename from packages/toolkit/src/tests/MiddlewareArray.typetest.ts
rename to packages/toolkit/src/tests/getDefaultMiddleware.typetest.ts
--- a/packages/toolkit/src/tests/MiddlewareArray.typetest.ts
+++ b/packages/toolkit/src/tests/getDefaultMiddleware.typetest.ts
@@ -1,4 +1,4 @@
-import { getDefaultMiddleware, configureStore } from '@reduxjs/toolkit'
+import { configureStore } from '@reduxjs/toolkit'
 import type { Middleware } from 'redux'
 
 declare const expectType: <T>(t: T) => T
diff --git a/packages/toolkit/src/tests/immutableStateInvariantMiddleware.test.ts b/packages/toolkit/src/tests/immutableStateInvariantMiddleware.test.ts
--- a/packages/toolkit/src/tests/immutableStateInvariantMiddleware.test.ts
+++ b/packages/toolkit/src/tests/immutableStateInvariantMiddleware.test.ts
@@ -1,8 +1,8 @@
 import type {
   Store,
   MiddlewareAPI,
-  Dispatch,
   ImmutableStateInvariantMiddlewareOptions,
+  Middleware,
 } from '@reduxjs/toolkit'
 import {
   createImmutableStateInvariantMiddleware,
@@ -10,7 +10,13 @@ import {
 } from '@reduxjs/toolkit'
 
 import { trackForMutations } from '@internal/immutableStateInvariantMiddleware'
-import { mockConsole, createConsole, getLog } from 'console-testing-library'
+import {
+  mockConsole,
+  createConsole,
+  getLog,
+} from 'console-testing-library/pure'
+
+type MWNext = Parameters<ReturnType<Middleware>>[0]
 
 describe('createImmutableStateInvariantMiddleware', () => {
   let state: { foo: { bar: number[]; baz: string } }
@@ -27,17 +33,17 @@ describe('createImmutableStateInvariantMiddleware', () => {
   })
 
   it('sends the action through the middleware chain', () => {
-    const next: Dispatch = (action) => ({ ...action, returned: true })
+    const next: MWNext = vi.fn()
     const dispatch = middleware()(next)
+    dispatch({ type: 'SOME_ACTION' })
 
-    expect(dispatch({ type: 'SOME_ACTION' })).toEqual({
+    expect(next).toHaveBeenCalledWith({
       type: 'SOME_ACTION',
-      returned: true,
     })
   })
 
   it('throws if mutating inside the dispatch', () => {
-    const next: Dispatch = (action) => {
+    const next: MWNext = (action) => {
       state.foo.bar.push(5)
       return action
     }
@@ -50,7 +56,7 @@ describe('createImmutableStateInvariantMiddleware', () => {
   })
 
   it('throws if mutating between dispatches', () => {
-    const next: Dispatch = (action) => action
+    const next: MWNext = (action) => action
 
     const dispatch = middleware()(next)
 
@@ -62,7 +68,7 @@ describe('createImmutableStateInvariantMiddleware', () => {
   })
 
   it('does not throw if not mutating inside the dispatch', () => {
-    const next: Dispatch = (action) => {
+    const next: MWNext = (action) => {
       state = { ...state, foo: { ...state.foo, baz: 'changed!' } }
       return action
     }
@@ -75,7 +81,7 @@ describe('createImmutableStateInvariantMiddleware', () => {
   })
 
   it('does not throw if not mutating between dispatches', () => {
-    const next: Dispatch = (action) => action
+    const next: MWNext = (action) => action
 
     const dispatch = middleware()(next)
 
@@ -87,7 +93,7 @@ describe('createImmutableStateInvariantMiddleware', () => {
   })
 
   it('works correctly with circular references', () => {
-    const next: Dispatch = (action) => action
+    const next: MWNext = (action) => action
 
     const dispatch = middleware()(next)
 
@@ -103,7 +109,7 @@ describe('createImmutableStateInvariantMiddleware', () => {
 
   it('respects "isImmutable" option', function () {
     const isImmutable = (value: any) => true
-    const next: Dispatch = (action) => {
+    const next: MWNext = (action) => {
       state.foo.bar.push(5)
       return action
     }
@@ -116,7 +122,7 @@ describe('createImmutableStateInvariantMiddleware', () => {
   })
 
   it('respects "ignoredPaths" option', () => {
-    const next: Dispatch = (action) => {
+    const next: MWNext = (action) => {
       state.foo.bar.push(5)
       return action
     }
@@ -134,23 +140,10 @@ describe('createImmutableStateInvariantMiddleware', () => {
     }).not.toThrow()
   })
 
-  it('alias "ignore" to "ignoredPath" and respects option', () => {
-    const next: Dispatch = (action) => {
-      state.foo.bar.push(5)
-      return action
-    }
-
-    const dispatch = middleware({ ignore: ['foo.bar'] })(next)
-
-    expect(() => {
-      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 next: MWNext = (action) => action
 
     const dispatch = middleware({ warnAfter: 4 })(next)
 
@@ -166,7 +159,7 @@ describe('createImmutableStateInvariantMiddleware', () => {
   })
 
   it('Should not print a warning if "next" takes too long', () => {
-    const next: Dispatch = (action) => {
+    const next: MWNext = (action) => {
       const started = Date.now()
       while (Date.now() - started < 8) {}
       return action
diff --git a/packages/toolkit/src/tests/injectableCombineReducers.example.ts b/packages/toolkit/src/tests/injectableCombineReducers.example.ts
deleted file mode 100644
--- a/packages/toolkit/src/tests/injectableCombineReducers.example.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/* eslint-disable import/first */
-// @ts-nocheck
-
-// reducer.ts or whatever
-
-import { combineSlices } from '@reduxjs/toolkit'
-
-import { sliceA } from 'fileA'
-import { sliceB } from 'fileB'
-import { lazySliceC } from 'fileC'
-import type { lazySliceD } from 'fileD'
-
-import { anotherReducer } from 'somewhere'
-
-export interface LazyLoadedSlices {}
-
-export const rootReducer = combineSlices(sliceA, sliceB, {
-  another: anotherReducer,
-}).withLazyLoadedSlices<LazyLoadedSlices>()
-/*
- results in a return type of
- {
-    [sliceA.name]: SliceAState,
-    [sliceB.name]: SliceBState,
-    another: AnotherState,
-    [lazySliceC.name]?: SliceCState, // see fileC.ts to understand why this appears here
-    [lazySliceD.name]?: SliceDState, // see fileD.ts to understand why this appears here
- }
- */
-
-// fileC.ts
-// "naive" approach
-
-import type { RootState } from './reducer';
-import { rootReducer } from './reducer'
-import { createSlice } from '@reduxjs/toolkit'
-
-interface SliceCState {
-  foo: string
-}
-
-declare module './reducer' {
-  export interface LazyLoadedSlices {
-    [lazySliceC.name]: SliceCState
-  }
-}
-
-export const lazySliceC = createSlice({
-  /* ... */
-})
-/**
- * Synchronously call `injectSlice` in file.
- */
-rootReducer.injectSlice(lazySliceC)
-
-// might want to add code for HMR as well here
-
-// this will still error - `lazySliceC` is optional here
-const naiveSelectFoo = (state: RootState) => state.lazySliceC.foo
-
-const selectFoo = rootReducer.withSlice(lazySliceC).selector((state) => {
-  // `lazySlice` is guaranteed to not be `undefined` here.
-  return state.lazySlice.foo
-})
diff --git a/packages/toolkit/src/tests/isPlainObject.test.ts b/packages/toolkit/src/tests/isPlainObject.test.ts
deleted file mode 100644
--- a/packages/toolkit/src/tests/isPlainObject.test.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { isPlainObject } from '@reduxjs/toolkit'
-import vm from 'vm'
-
-describe('isPlainObject', () => {
-  it('returns true only if plain object', () => {
-    class Test {
-      prop: number
-      constructor() {
-        this.prop = 1
-      }
-    }
-
-    const sandbox = { fromAnotherRealm: false }
-    vm.runInNewContext('fromAnotherRealm = {}', sandbox)
-
-    expect(isPlainObject(sandbox.fromAnotherRealm)).toBe(true)
-    expect(isPlainObject(new Test())).toBe(false)
-    expect(isPlainObject(new Date())).toBe(false)
-    expect(isPlainObject([1, 2, 3])).toBe(false)
-    expect(isPlainObject(null)).toBe(false)
-    expect(isPlainObject(undefined)).toBe(false)
-    expect(isPlainObject({ x: 1, y: 2 })).toBe(true)
-    expect(isPlainObject(Object.create(null))).toBe(true)
-  })
-})
diff --git a/packages/toolkit/src/tests/mapBuilders.typetest.ts b/packages/toolkit/src/tests/mapBuilders.typetest.ts
--- a/packages/toolkit/src/tests/mapBuilders.typetest.ts
+++ b/packages/toolkit/src/tests/mapBuilders.typetest.ts
@@ -1,11 +1,11 @@
 import type { SerializedError } from '@internal/createAsyncThunk'
 import { createAsyncThunk } from '@internal/createAsyncThunk'
 import { executeReducerBuilderCallback } from '@internal/mapBuilders'
-import type { AnyAction } from '@reduxjs/toolkit'
+import type { UnknownAction } from '@reduxjs/toolkit'
 import { createAction } from '@reduxjs/toolkit'
 import { expectExactType, expectType } from './helpers'
 
-/** Test:  alternative builder callback for actionMap */
+/** Test:  builder callback for actionMap */
 {
   const increment = createAction<number, 'increment'>('increment')
   const decrement = createAction<number, 'decrement'>('decrement')
@@ -66,16 +66,16 @@ import { expectExactType, expectType } from './helpers'
         (action): action is PredicateWithoutTypeProperty => true,
         (state, action) => {
           expectType<PredicateWithoutTypeProperty>(action)
-          expectType<AnyAction>(action)
+          expectType<UnknownAction>(action)
         }
       )
     }
 
-    // action type defaults to AnyAction if no type predicate matcher is passed
+    // action type defaults to UnknownAction if no type predicate matcher is passed
     builder.addMatcher(
       () => true,
       (state, action) => {
-        expectExactType({} as AnyAction)(action)
+        expectExactType({} as UnknownAction)(action)
       }
     )
 
@@ -84,7 +84,7 @@ import { expectExactType, expectType } from './helpers'
       () => true,
       (state, action) => {
         expectType<{ foo: boolean }>(action)
-        expectType<AnyAction>(action)
+        expectType<UnknownAction>(action)
       }
     )
 
@@ -98,14 +98,14 @@ import { expectExactType, expectType } from './helpers'
         expectType<ReturnType<typeof decrement>>(action)
       })
 
-    // addCase().addDefaultCase() is possible, action type is AnyAction
+    // addCase().addDefaultCase() is possible, action type is UnknownAction
     builder
       .addCase(
         'increment',
         (state, action: ReturnType<typeof increment>) => state
       )
       .addDefaultCase((state, action) => {
-        expectType<AnyAction>(action)
+        expectType<UnknownAction>(action)
       })
 
     {
diff --git a/packages/toolkit/src/tests/matchers.test.ts b/packages/toolkit/src/tests/matchers.test.ts
--- a/packages/toolkit/src/tests/matchers.test.ts
+++ b/packages/toolkit/src/tests/matchers.test.ts
@@ -1,4 +1,5 @@
-import type { ThunkAction, AnyAction } from '@reduxjs/toolkit'
+import { vi } from 'vitest'
+import type { ThunkAction, UnknownAction } from '@reduxjs/toolkit'
 import {
   isAllOf,
   isAnyOf,
@@ -12,7 +13,7 @@ import {
   createReducer,
 } from '@reduxjs/toolkit'
 
-const thunk: ThunkAction<any, any, any, AnyAction> = () => {}
+const thunk: ThunkAction<any, any, any, UnknownAction> = () => {}
 
 describe('isAnyOf', () => {
   it('returns true only if any matchers match (match function)', () => {
@@ -282,8 +283,8 @@ describe('isRejectedWithValue', () => {
     )
     expect(isRejectedWithValue()(rejectedAction)).toBe(false)
 
-    const getState = jest.fn(() => ({}))
-    const dispatch = jest.fn((x: any) => x)
+    const getState = vi.fn(() => ({}))
+    const dispatch = vi.fn((x: any) => x)
     const extra = {}
 
     // note: doesn't throw because we don't unwrap it
@@ -321,8 +322,8 @@ describe('isRejectedWithValue', () => {
       // rejected-with-value is a narrower requirement than rejected
       expect(matchAC(rejectedAction)).toBe(false)
 
-      const getState = jest.fn(() => ({}))
-      const dispatch = jest.fn((x: any) => x)
+      const getState = vi.fn(() => ({}))
+      const dispatch = vi.fn((x: any) => x)
       const extra = {}
 
       // note: doesn't throw because we don't unwrap it
diff --git a/packages/toolkit/src/tests/matchers.typetest.ts b/packages/toolkit/src/tests/matchers.typetest.ts
--- a/packages/toolkit/src/tests/matchers.typetest.ts
+++ b/packages/toolkit/src/tests/matchers.typetest.ts
@@ -1,6 +1,5 @@
 import { expectExactType, expectUnknown } from './helpers'
-import { IsUnknown } from '@internal/tsHelpers'
-import type { AnyAction } from 'redux'
+import type { UnknownAction } from 'redux'
 import type { SerializedError } from '../../src'
 import {
   createAction,
@@ -19,7 +18,7 @@ import {
 /*
  * Test: isAnyOf correctly narrows types when used with action creators
  */
-function isAnyOfActionTest(action: AnyAction) {
+function isAnyOfActionTest(action: UnknownAction) {
   const actionA = createAction('a', () => {
     return {
       payload: {
@@ -52,7 +51,7 @@ function isAnyOfActionTest(action: AnyAction) {
 /*
  * Test: isAnyOf correctly narrows types when used with async thunks
  */
-function isAnyOfThunkTest(action: AnyAction) {
+function isAnyOfThunkTest(action: UnknownAction) {
   const asyncThunk1 = createAsyncThunk<{ prop1: number; prop3: number }>(
     'asyncThunk1',
 
@@ -89,7 +88,7 @@ function isAnyOfThunkTest(action: AnyAction) {
 /*
  * Test: isAnyOf correctly narrows types when used with type guards
  */
-function isAnyOfTypeGuardTest(action: AnyAction) {
+function isAnyOfTypeGuardTest(action: UnknownAction) {
   interface ActionA {
     type: 'a'
     payload: {
@@ -141,7 +140,7 @@ const isSpecialAction = (v: any): v is SpecialAction => {
  * Test: isAllOf correctly narrows types when used with action creators
  *       and type guards
  */
-function isAllOfActionTest(action: AnyAction) {
+function isAllOfActionTest(action: UnknownAction) {
   const actionA = createAction('a', () => {
     return {
       payload: {
@@ -166,7 +165,7 @@ function isAllOfActionTest(action: AnyAction) {
  * Test: isAllOf correctly narrows types when used with async thunks
  *       and type guards
  */
-function isAllOfThunkTest(action: AnyAction) {
+function isAllOfThunkTest(action: UnknownAction) {
   const asyncThunk1 = createAsyncThunk<{ prop1: number; prop3: number }>(
     'asyncThunk1',
 
@@ -192,7 +191,7 @@ function isAllOfThunkTest(action: AnyAction) {
 /*
  * Test: isAnyOf correctly narrows types when used with type guards
  */
-function isAllOfTypeGuardTest(action: AnyAction) {
+function isAllOfTypeGuardTest(action: UnknownAction) {
   interface ActionA {
     type: 'a'
     payload: {
@@ -219,7 +218,7 @@ function isAllOfTypeGuardTest(action: AnyAction) {
 /*
  * Test: isPending correctly narrows types
  */
-function isPendingTest(action: AnyAction) {
+function isPendingTest(action: UnknownAction) {
   if (isPending(action)) {
     expectExactType<undefined>(action.payload)
     // @ts-expect-error
@@ -238,7 +237,7 @@ function isPendingTest(action: AnyAction) {
 /*
  * Test: isRejected correctly narrows types
  */
-function isRejectedTest(action: AnyAction) {
+function isRejectedTest(action: UnknownAction) {
   if (isRejected(action)) {
     // might be there if rejected with payload
     expectUnknown(action.payload)
@@ -257,7 +256,7 @@ function isRejectedTest(action: AnyAction) {
 /*
  * Test: isFulfilled correctly narrows types
  */
-function isFulfilledTest(action: AnyAction) {
+function isFulfilledTest(action: UnknownAction) {
   if (isFulfilled(action)) {
     expectUnknown(action.payload)
     // @ts-expect-error
@@ -275,7 +274,7 @@ function isFulfilledTest(action: AnyAction) {
 /*
  * Test: isAsyncThunkAction correctly narrows types
  */
-function isAsyncThunkActionTest(action: AnyAction) {
+function isAsyncThunkActionTest(action: UnknownAction) {
   if (isAsyncThunkAction(action)) {
     expectUnknown(action.payload)
     // do not expect an error property because pending/fulfilled lack it
@@ -296,7 +295,7 @@ function isAsyncThunkActionTest(action: AnyAction) {
 /*
  * Test: isRejectedWithValue correctly narrows types
  */
-function isRejectedWithValueTest(action: AnyAction) {
+function isRejectedWithValueTest(action: UnknownAction) {
   if (isRejectedWithValue(action)) {
     expectUnknown(action.payload)
     expectExactType<SerializedError>(action.error)
diff --git a/packages/toolkit/src/tests/serializableStateInvariantMiddleware.test.ts b/packages/toolkit/src/tests/serializableStateInvariantMiddleware.test.ts
--- a/packages/toolkit/src/tests/serializableStateInvariantMiddleware.test.ts
+++ b/packages/toolkit/src/tests/serializableStateInvariantMiddleware.test.ts
@@ -3,13 +3,14 @@ import {
   createConsole,
   getLog,
 } from 'console-testing-library/pure'
-import type { AnyAction, Reducer } from '@reduxjs/toolkit'
+import type { Reducer } from '@reduxjs/toolkit'
 import {
   createNextState,
   configureStore,
   createSerializableStateInvariantMiddleware,
   findNonSerializableValue,
   isPlain,
+  Tuple,
 } from '@reduxjs/toolkit'
 import { isNestedFrozen } from '@internal/serializableStateInvariantMiddleware'
 
@@ -100,18 +101,19 @@ describe('serializableStateInvariantMiddleware', () => {
 
     const store = configureStore({
       reducer,
-      middleware: [serializableStateInvariantMiddleware],
+      middleware: () => new Tuple(serializableStateInvariantMiddleware),
     })
 
-    const type = Symbol.for('SOME_CONSTANT')
-    const dispatchedAction = { type }
+    const symbol = Symbol.for('SOME_CONSTANT')
+    const dispatchedAction = { type: 'an-action', payload: symbol }
 
     store.dispatch(dispatchedAction)
 
     expect(getLog().log).toMatchInlineSnapshot(`
-      "A non-serializable value was detected in an action, in the path: \`type\`. Value: Symbol(SOME_CONSTANT) 
+      "A non-serializable value was detected in an action, in the path: \`payload\`. Value: Symbol(SOME_CONSTANT) 
       Take a look at the logic that dispatched this action:  Object {
-        \\"type\\": Symbol(SOME_CONSTANT),
+        \\"payload\\": Symbol(SOME_CONSTANT),
+        \\"type\\": \\"an-action\\",
       } 
       (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)"
@@ -146,7 +148,7 @@ describe('serializableStateInvariantMiddleware', () => {
       reducer: {
         testSlice: reducer,
       },
-      middleware: [serializableStateInvariantMiddleware],
+      middleware: () => new Tuple(serializableStateInvariantMiddleware),
     })
 
     store.dispatch({ type: ACTION_TYPE })
@@ -206,7 +208,7 @@ describe('serializableStateInvariantMiddleware', () => {
         reducer: {
           testSlice: reducer,
         },
-        middleware: [serializableStateInvariantMiddleware],
+        middleware: () => new Tuple(serializableStateInvariantMiddleware),
       })
 
       store.dispatch({ type: ACTION_TYPE })
@@ -253,7 +255,7 @@ describe('serializableStateInvariantMiddleware', () => {
         reducer: {
           testSlice: reducer,
         },
-        middleware: [serializableStateInvariantMiddleware],
+        middleware: () => new Tuple(serializableStateInvariantMiddleware),
       })
 
       store.dispatch({ type: ACTION_TYPE })
@@ -297,7 +299,7 @@ describe('serializableStateInvariantMiddleware', () => {
       reducer: {
         testSlice: reducer,
       },
-      middleware: [serializableStateInvariantMiddleware],
+      middleware: () => new Tuple(serializableStateInvariantMiddleware),
     })
 
     store.dispatch({ type: ACTION_TYPE })
@@ -321,7 +323,7 @@ describe('serializableStateInvariantMiddleware', () => {
 
     const store = configureStore({
       reducer: () => ({}),
-      middleware: [serializableStateMiddleware],
+      middleware: () => new Tuple(serializableStateMiddleware),
     })
 
     expect(numTimesCalled).toBe(0)
@@ -346,7 +348,8 @@ describe('serializableStateInvariantMiddleware', () => {
     it('default value: meta.arg', () => {
       configureStore({
         reducer,
-        middleware: [createSerializableStateInvariantMiddleware()],
+        middleware: () =>
+          new Tuple(createSerializableStateInvariantMiddleware()),
       }).dispatch({ type: 'test', meta: { arg: nonSerializableValue } })
 
       expect(getLog().log).toMatchInlineSnapshot(`""`)
@@ -355,11 +358,12 @@ describe('serializableStateInvariantMiddleware', () => {
     it('default value can be overridden', () => {
       configureStore({
         reducer,
-        middleware: [
-          createSerializableStateInvariantMiddleware({
-            ignoredActionPaths: [],
-          }),
-        ],
+        middleware: () =>
+          new Tuple(
+            createSerializableStateInvariantMiddleware({
+              ignoredActionPaths: [],
+            })
+          ),
       }).dispatch({ type: 'test', meta: { arg: nonSerializableValue } })
 
       expect(getLog().log).toMatchInlineSnapshot(`
@@ -378,11 +382,12 @@ describe('serializableStateInvariantMiddleware', () => {
     it('can specify (multiple) different values', () => {
       configureStore({
         reducer,
-        middleware: [
-          createSerializableStateInvariantMiddleware({
-            ignoredActionPaths: ['payload', 'meta.arg'],
-          }),
-        ],
+        middleware: () =>
+          new Tuple(
+            createSerializableStateInvariantMiddleware({
+              ignoredActionPaths: ['payload', 'meta.arg'],
+            })
+          ),
       }).dispatch({
         type: 'test',
         payload: { arg: nonSerializableValue },
@@ -395,11 +400,12 @@ describe('serializableStateInvariantMiddleware', () => {
     it('can specify regexp', () => {
       configureStore({
         reducer,
-        middleware: [
-          createSerializableStateInvariantMiddleware({
-            ignoredActionPaths: [/^payload\..*$/],
-          }),
-        ],
+        middleware: () =>
+          new Tuple(
+            createSerializableStateInvariantMiddleware({
+              ignoredActionPaths: [/^payload\..*$/],
+            })
+          ),
       }).dispatch({
         type: 'test',
         payload: { arg: nonSerializableValue },
@@ -423,7 +429,7 @@ describe('serializableStateInvariantMiddleware', () => {
 
     const store = configureStore({
       reducer: () => ({}),
-      middleware: [serializableStateMiddleware],
+      middleware: () => new Tuple(serializableStateMiddleware),
     })
 
     expect(numTimesCalled).toBe(0)
@@ -486,7 +492,7 @@ describe('serializableStateInvariantMiddleware', () => {
       reducer: {
         testSlice: reducer,
       },
-      middleware: [serializableStateInvariantMiddleware],
+      middleware: () => new Tuple(serializableStateInvariantMiddleware),
     })
 
     store.dispatch({ type: ACTION_TYPE })
@@ -505,15 +511,16 @@ describe('serializableStateInvariantMiddleware', () => {
     const reducer = () => badValue
     const store = configureStore({
       reducer,
-      middleware: [
-        createSerializableStateInvariantMiddleware({
-          isSerializable: () => {
-            numTimesCalled++
-            return true
-          },
-          ignoreState: true,
-        }),
-      ],
+      middleware: () =>
+        new Tuple(
+          createSerializableStateInvariantMiddleware({
+            isSerializable: () => {
+              numTimesCalled++
+              return true
+            },
+            ignoreState: true,
+          })
+        ),
     })
 
     expect(numTimesCalled).toBe(0)
@@ -532,16 +539,17 @@ describe('serializableStateInvariantMiddleware', () => {
     const reducer = () => badValue
     const store = configureStore({
       reducer,
-      middleware: [
-        createSerializableStateInvariantMiddleware({
-          isSerializable: () => {
-            numTimesCalled++
-            return true
-          },
-          ignoreState: true,
-          ignoreActions: true,
-        }),
-      ],
+      middleware: () =>
+        new Tuple(
+          createSerializableStateInvariantMiddleware({
+            isSerializable: () => {
+              numTimesCalled++
+              return true
+            },
+            ignoreState: true,
+            ignoreActions: true,
+          })
+        ),
     })
 
     expect(numTimesCalled).toBe(0)
@@ -564,7 +572,7 @@ describe('serializableStateInvariantMiddleware', () => {
       reducer: {
         testSlice: reducer,
       },
-      middleware: [serializableStateInvariantMiddleware],
+      middleware: () => new Tuple(serializableStateInvariantMiddleware),
     })
 
     store.dispatch({
@@ -590,7 +598,7 @@ describe('serializableStateInvariantMiddleware', () => {
       reducer: {
         testSlice: reducer,
       },
-      middleware: [serializableStateInvariantMiddleware],
+      middleware: () => new Tuple(serializableStateInvariantMiddleware),
     })
 
     store.dispatch({ type: 'SOME_ACTION' })
@@ -614,7 +622,7 @@ describe('serializableStateInvariantMiddleware', () => {
         if (action.type === 'SET_STATE') return action.payload
         return state
       },
-      middleware: [serializableStateInvariantMiddleware],
+      middleware: () => new Tuple(serializableStateInvariantMiddleware),
     })
 
     const state = createNextState([], () =>