diff --git a/docs/api/configureStore.mdx b/docs/api/configureStore.mdx index 0c3f252c45..fe02e3eb9d 100644 --- a/docs/api/configureStore.mdx +++ b/docs/api/configureStore.mdx @@ -64,6 +64,11 @@ interface ConfigureStoreOptions< */ devTools?: boolean | DevToolsOptions + /** + * Whether to check for duplicate middleware instances. Defaults to `true`. + */ + duplicateMiddlewareCheck?: boolean + /** * The initial state, same as Redux's createStore. * You may optionally specify it to hydrate the state @@ -142,6 +147,12 @@ a list of the specific options that are available. Defaults to `true`. +### `duplicateMiddlewareCheck` + +If enabled, the store will check the final middleware array to see if there are any duplicate middleware references. This will catch issues like accidentally adding the same RTK Query API middleware twice (such as adding both the base API middleware and an injected API middleware, which are actually the exact same function reference). + +Defaults to `true`. + #### `trace` The Redux DevTools Extension recently added [support for showing action stack traces](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/Features/Trace.md) that show exactly where each action was dispatched. diff --git a/packages/toolkit/src/configureStore.ts b/packages/toolkit/src/configureStore.ts index 7a4e47b205..70529fabd2 100644 --- a/packages/toolkit/src/configureStore.ts +++ b/packages/toolkit/src/configureStore.ts @@ -66,6 +66,11 @@ export interface ConfigureStoreOptions< */ devTools?: boolean | DevToolsOptions + /** + * Whether to check for duplicate middleware instances. Defaults to `true`. + */ + duplicateMiddlewareCheck?: boolean + /** * The initial state, same as Redux's createStore. * You may optionally specify it to hydrate the state @@ -128,6 +133,7 @@ export function configureStore< reducer = undefined, middleware, devTools = true, + duplicateMiddlewareCheck = true, preloadedState = undefined, enhancers = undefined, } = options || {} @@ -176,6 +182,18 @@ export function configureStore< ) } + if (process.env.NODE_ENV !== 'production' && duplicateMiddlewareCheck) { + let middlewareReferences = new Set>() + finalMiddleware.forEach((middleware) => { + if (middlewareReferences.has(middleware)) { + throw new Error( + 'Duplicate middleware found. Ensure that each middleware is only included once', + ) + } + middlewareReferences.add(middleware) + }) + } + let finalCompose = compose if (devTools) { diff --git a/packages/toolkit/src/query/tests/injectEndpoints.test.tsx b/packages/toolkit/src/query/tests/injectEndpoints.test.tsx index 14bc43eb7e..89404bff8e 100644 --- a/packages/toolkit/src/query/tests/injectEndpoints.test.tsx +++ b/packages/toolkit/src/query/tests/injectEndpoints.test.tsx @@ -1,4 +1,5 @@ import { noop } from '@internal/listenerMiddleware/utils' +import { configureStore } from '@internal/configureStore' import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query' const api = createApi({ @@ -91,4 +92,30 @@ describe('injectEndpoints', () => { expect(consoleErrorSpy).not.toHaveBeenCalled() }) + + test('adding the same middleware to the store twice throws an error', () => { + // Strictly speaking this is a duplicate of the tests in configureStore.test.ts, + // but this helps confirm that we throw the error for adding + // the same API middleware twice. + const extendedApi = api.injectEndpoints({ + endpoints: (build) => ({ + injected: build.query({ + query: () => '/success', + }), + }), + }) + + const makeStore = () => + configureStore({ + reducer: { + api: api.reducer, + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat(api.middleware, extendedApi.middleware), + }) + + expect(makeStore).toThrowError( + 'Duplicate middleware found. Ensure that each middleware is only included once', + ) + }) }) diff --git a/packages/toolkit/src/tests/configureStore.test.ts b/packages/toolkit/src/tests/configureStore.test.ts index c6ca8fc6ca..de78a80700 100644 --- a/packages/toolkit/src/tests/configureStore.test.ts +++ b/packages/toolkit/src/tests/configureStore.test.ts @@ -1,5 +1,5 @@ import * as DevTools from '@internal/devtoolsExtension' -import type { StoreEnhancer } from '@reduxjs/toolkit' +import type { Middleware, StoreEnhancer } from '@reduxjs/toolkit' import { Tuple } from '@reduxjs/toolkit' import type * as Redux from 'redux' import { vi } from 'vitest' @@ -130,6 +130,37 @@ describe('configureStore', async () => { }) }) + describe('given any middleware', () => { + const exampleMiddleware: Middleware = () => (next) => (action) => + next(action) + it('throws an error by default if there are duplicate middleware', () => { + const makeStore = () => { + return configureStore({ + reducer, + middleware: (gDM) => + gDM().concat(exampleMiddleware, exampleMiddleware), + }) + } + + expect(makeStore).toThrowError( + 'Duplicate middleware found. Ensure that each middleware is only included once', + ) + }) + + it('does not throw a duplicate middleware error if duplicateMiddlewareCheck is disabled', () => { + const makeStore = () => { + return configureStore({ + reducer, + middleware: (gDM) => + gDM().concat(exampleMiddleware, exampleMiddleware), + duplicateMiddlewareCheck: false, + }) + } + + expect(makeStore).not.toThrowError() + }) + }) + describe('given a middleware creation function that returns undefined', () => { it('throws an error', () => { const invalidBuilder = vi.fn((getDefaultMiddleware) => undefined as any)