Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/api/configureStore.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions packages/toolkit/src/configureStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -128,6 +133,7 @@ export function configureStore<
reducer = undefined,
middleware,
devTools = true,
duplicateMiddlewareCheck = true,
preloadedState = undefined,
enhancers = undefined,
} = options || {}
Expand Down Expand Up @@ -176,6 +182,18 @@ export function configureStore<
)
}

if (process.env.NODE_ENV !== 'production' && duplicateMiddlewareCheck) {
let middlewareReferences = new Set<Middleware<any, S>>()
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) {
Expand Down
27 changes: 27 additions & 0 deletions packages/toolkit/src/query/tests/injectEndpoints.test.tsx
Original file line number Diff line number Diff line change
@@ -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({
Expand Down Expand Up @@ -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<unknown, string>({
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',
)
})
})
33 changes: 32 additions & 1 deletion packages/toolkit/src/tests/configureStore.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -130,6 +130,37 @@ describe('configureStore', async () => {
})
})

describe('given any middleware', () => {
const exampleMiddleware: Middleware<any, any> = () => (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)
Expand Down