Skip to content

Commit e730507

Browse files
authored
Add duplicate middleware dev check to configureStore (#4927)
1 parent 9881b1a commit e730507

File tree

4 files changed

+88
-1
lines changed

4 files changed

+88
-1
lines changed

docs/api/configureStore.mdx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ interface ConfigureStoreOptions<
6464
*/
6565
devTools?: boolean | DevToolsOptions
6666

67+
/**
68+
* Whether to check for duplicate middleware instances. Defaults to `true`.
69+
*/
70+
duplicateMiddlewareCheck?: boolean
71+
6772
/**
6873
* The initial state, same as Redux's createStore.
6974
* You may optionally specify it to hydrate the state
@@ -142,6 +147,12 @@ a list of the specific options that are available.
142147

143148
Defaults to `true`.
144149

150+
### `duplicateMiddlewareCheck`
151+
152+
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).
153+
154+
Defaults to `true`.
155+
145156
#### `trace`
146157

147158
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.

packages/toolkit/src/configureStore.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ export interface ConfigureStoreOptions<
6666
*/
6767
devTools?: boolean | DevToolsOptions
6868

69+
/**
70+
* Whether to check for duplicate middleware instances. Defaults to `true`.
71+
*/
72+
duplicateMiddlewareCheck?: boolean
73+
6974
/**
7075
* The initial state, same as Redux's createStore.
7176
* You may optionally specify it to hydrate the state
@@ -128,6 +133,7 @@ export function configureStore<
128133
reducer = undefined,
129134
middleware,
130135
devTools = true,
136+
duplicateMiddlewareCheck = true,
131137
preloadedState = undefined,
132138
enhancers = undefined,
133139
} = options || {}
@@ -176,6 +182,18 @@ export function configureStore<
176182
)
177183
}
178184

185+
if (process.env.NODE_ENV !== 'production' && duplicateMiddlewareCheck) {
186+
let middlewareReferences = new Set<Middleware<any, S>>()
187+
finalMiddleware.forEach((middleware) => {
188+
if (middlewareReferences.has(middleware)) {
189+
throw new Error(
190+
'Duplicate middleware found. Ensure that each middleware is only included once',
191+
)
192+
}
193+
middlewareReferences.add(middleware)
194+
})
195+
}
196+
179197
let finalCompose = compose
180198

181199
if (devTools) {

packages/toolkit/src/query/tests/injectEndpoints.test.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { noop } from '@internal/listenerMiddleware/utils'
2+
import { configureStore } from '@internal/configureStore'
23
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
34

45
const api = createApi({
@@ -91,4 +92,30 @@ describe('injectEndpoints', () => {
9192

9293
expect(consoleErrorSpy).not.toHaveBeenCalled()
9394
})
95+
96+
test('adding the same middleware to the store twice throws an error', () => {
97+
// Strictly speaking this is a duplicate of the tests in configureStore.test.ts,
98+
// but this helps confirm that we throw the error for adding
99+
// the same API middleware twice.
100+
const extendedApi = api.injectEndpoints({
101+
endpoints: (build) => ({
102+
injected: build.query<unknown, string>({
103+
query: () => '/success',
104+
}),
105+
}),
106+
})
107+
108+
const makeStore = () =>
109+
configureStore({
110+
reducer: {
111+
api: api.reducer,
112+
},
113+
middleware: (getDefaultMiddleware) =>
114+
getDefaultMiddleware().concat(api.middleware, extendedApi.middleware),
115+
})
116+
117+
expect(makeStore).toThrowError(
118+
'Duplicate middleware found. Ensure that each middleware is only included once',
119+
)
120+
})
94121
})

packages/toolkit/src/tests/configureStore.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as DevTools from '@internal/devtoolsExtension'
2-
import type { StoreEnhancer } from '@reduxjs/toolkit'
2+
import type { Middleware, StoreEnhancer } from '@reduxjs/toolkit'
33
import { Tuple } from '@reduxjs/toolkit'
44
import type * as Redux from 'redux'
55
import { vi } from 'vitest'
@@ -130,6 +130,37 @@ describe('configureStore', async () => {
130130
})
131131
})
132132

133+
describe('given any middleware', () => {
134+
const exampleMiddleware: Middleware<any, any> = () => (next) => (action) =>
135+
next(action)
136+
it('throws an error by default if there are duplicate middleware', () => {
137+
const makeStore = () => {
138+
return configureStore({
139+
reducer,
140+
middleware: (gDM) =>
141+
gDM().concat(exampleMiddleware, exampleMiddleware),
142+
})
143+
}
144+
145+
expect(makeStore).toThrowError(
146+
'Duplicate middleware found. Ensure that each middleware is only included once',
147+
)
148+
})
149+
150+
it('does not throw a duplicate middleware error if duplicateMiddlewareCheck is disabled', () => {
151+
const makeStore = () => {
152+
return configureStore({
153+
reducer,
154+
middleware: (gDM) =>
155+
gDM().concat(exampleMiddleware, exampleMiddleware),
156+
duplicateMiddlewareCheck: false,
157+
})
158+
}
159+
160+
expect(makeStore).not.toThrowError()
161+
})
162+
})
163+
133164
describe('given a middleware creation function that returns undefined', () => {
134165
it('throws an error', () => {
135166
const invalidBuilder = vi.fn((getDefaultMiddleware) => undefined as any)

0 commit comments

Comments
 (0)