Skip to content
Open
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
24 changes: 2 additions & 22 deletions packages/toolkit/src/query/core/buildMiddleware/devMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { InternalHandlerBuilder } from './types'
import { registerMiddleware } from './registration'

export const buildDevCheckHandler: InternalHandlerBuilder = ({
api,
Expand All @@ -7,28 +8,7 @@ export const buildDevCheckHandler: InternalHandlerBuilder = ({
}) => {
return (action, mwApi) => {
if (api.util.resetApiState.match(action)) {
// dispatch after api reset
mwApi.dispatch(api.internalActions.middlewareRegistered(apiUid))
}

if (
typeof process !== 'undefined' &&
process.env.NODE_ENV === 'development'
) {
if (
api.internalActions.middlewareRegistered.match(action) &&
action.payload === apiUid &&
mwApi.getState()[reducerPath]?.config?.middlewareRegistered ===
'conflict'
) {
console.warn(`There is a mismatch between slice and middleware for the reducerPath "${reducerPath}".
You can only have one api per reducer path, this will lead to crashes in various situations!${
reducerPath === 'api'
? `
If you have multiple apis, you *have* to specify the reducerPath option when using createApi!`
: ''
}`)
}
registerMiddleware(api, mwApi, apiUid, reducerPath)
}
}
}
9 changes: 5 additions & 4 deletions packages/toolkit/src/query/core/buildMiddleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { buildDevCheckHandler } from './devMiddleware'
import { buildInvalidationByTagsHandler } from './invalidationByTags'
import { buildPollingHandler } from './polling'
import { buildQueryLifecycleHandler } from './queryLifecycle'
import { registerMiddleware } from './registration'
import type {
BuildMiddlewareInput,
InternalHandlerBuilder,
Expand Down Expand Up @@ -97,14 +98,14 @@ export function buildMiddleware<
if (!isAction(action)) {
return next(action)
}

const mwApiWithNext = { ...mwApi, next }

if (!initialized) {
initialized = true
// dispatch before any other action
mwApi.dispatch(api.internalActions.middlewareRegistered(apiUid))
registerMiddleware(api, mwApiWithNext, apiUid, reducerPath)
}

const mwApiWithNext = { ...mwApi, next }

const stateBefore = mwApi.getState()

const [actionShouldContinue, internalProbeResult] =
Expand Down
40 changes: 40 additions & 0 deletions packages/toolkit/src/query/core/buildMiddleware/registration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { SubMiddlewareApi } from './types'

type MiddlewareRegistrationAction = {
type: string
payload: string
}

type MiddlewareRegistrationApi = {
internalActions: {
middlewareRegistered(apiUid: string): MiddlewareRegistrationAction
}
}

export function registerMiddleware(
api: MiddlewareRegistrationApi,
mwApi: SubMiddlewareApi & {
next(action: MiddlewareRegistrationAction): unknown
},
apiUid: string,
reducerPath: string,
) {
// Forward the registration action through the remaining chain only.
// Restarting from the top-level dispatch can overflow the call stack
// when many RTK Query middlewares initialize on the same action.
mwApi.next(api.internalActions.middlewareRegistered(apiUid))

if (
typeof process !== 'undefined' &&
process.env.NODE_ENV === 'development' &&
mwApi.getState()[reducerPath]?.config?.middlewareRegistered === 'conflict'
) {
console.warn(`There is a mismatch between slice and middleware for the reducerPath "${reducerPath}".
You can only have one api per reducer path, this will lead to crashes in various situations!${
reducerPath === 'api'
? `
If you have multiple apis, you *have* to specify the reducerPath option when using createApi!`
: ''
}`)
}
}
33 changes: 33 additions & 0 deletions packages/toolkit/src/query/tests/buildMiddleware.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { configureStore } from '@reduxjs/toolkit'
import { createApi } from '@reduxjs/toolkit/query'
import { delay } from 'msw'
import { actionsReducer, setupApiStore } from '../../tests/utils/helpers'
Expand Down Expand Up @@ -264,3 +265,35 @@ it('does not leak subscription state between multiple stores using the same API

expect(store2InternalSubs.size).toBe(0)
})

it(
'initializes a large RTK Query middleware chain without overflowing the stack',
{ timeout: 120_000 },
async () => {
const middlewareChain = Array.from({ length: 1000 }, () => {
const middleware = api.middleware
return ((mwApi) => middleware(mwApi)) as typeof api.middleware
})

const store = configureStore({
reducer: { [api.reducerPath]: api.reducer },
middleware: (gdm) =>
gdm({
serializableCheck: false,
immutableCheck: false,
}).concat(...middlewareChain),
})

await store.dispatch(getBanana.initiate(1))

expect(store.getState()[api.reducerPath].config.middlewareRegistered).toBe(
true,
)
expect(store.getState()[api.reducerPath].queries['getBanana(1)']).toEqual(
expect.objectContaining({
data: { url: 'banana/1' },
status: 'fulfilled',
}),
)
},
)
Loading