Skip to content

Commit 6c197df

Browse files
authored
dev warnings for missing reducer or middleware (#1098)
general robustness
1 parent c88b10b commit 6c197df

14 files changed

+395
-119
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ module.exports = {
2424
rules: {
2525
'@typescript-eslint/no-unused-expressions': 'off',
2626
'no-lone-blocks': 'off',
27+
'no-sequences': 'off',
2728
},
2829
},
2930
],

src/query/core/apiState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ export type ConfigState<ReducerPath> = RefetchConfigOptions & {
241241
reducerPath: ReducerPath
242242
online: boolean
243243
focused: boolean
244+
middlewareRegistered: boolean
244245
} & ModifiableConfigState
245246

246247
export type ModifiableConfigState = {

src/query/core/buildInitiate.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@ import type {
1010
AnyAction,
1111
AsyncThunk,
1212
ThunkAction,
13-
SerializedError} from '@reduxjs/toolkit';
14-
import {
15-
unwrapResult
13+
SerializedError,
1614
} from '@reduxjs/toolkit'
17-
import type { QuerySubState, SubscriptionOptions } from './apiState'
15+
import { unwrapResult } from '@reduxjs/toolkit'
16+
import type { QuerySubState, SubscriptionOptions, RootState } from './apiState'
1817
import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
1918
import type { Api } from '../apiTypes'
2019
import type { ApiEndpointQuery } from './module'
@@ -198,6 +197,23 @@ export function buildInitiate({
198197
} = api.internalActions
199198
return { buildInitiateQuery, buildInitiateMutation }
200199

200+
function middlewareWarning(getState: () => RootState<{}, string, string>) {
201+
if (process.env.NODE_ENV !== 'production') {
202+
if ((middlewareWarning as any).triggered) return
203+
const registered = getState()[api.reducerPath]?.config
204+
?.middlewareRegistered
205+
if (registered !== undefined) {
206+
;(middlewareWarning as any).triggered = true
207+
}
208+
if (registered === false) {
209+
console.warn(
210+
`Warning: Middleware for RTK-Query API at reducerPath "${api.reducerPath}" has not been added to the store.
211+
Features like automatic cache collection, automatic refetching etc. will not be available.`
212+
)
213+
}
214+
}
215+
}
216+
201217
function buildInitiateQuery(
202218
endpointName: string,
203219
endpointDefinition: QueryDefinition<any, any, any, any>
@@ -221,6 +237,7 @@ export function buildInitiate({
221237
startedTimeStamp: Date.now(),
222238
})
223239
const thunkResult = dispatch(thunk)
240+
middlewareWarning(getState)
224241
const { requestId, abort } = thunkResult
225242
const statePromise = Object.assign(
226243
thunkResult.then(() =>
@@ -275,6 +292,7 @@ export function buildInitiate({
275292
startedTimeStamp: Date.now(),
276293
})
277294
const thunkResult = dispatch(thunk)
295+
middlewareWarning(getState)
278296
const { requestId, abort } = thunkResult
279297
const returnValuePromise = thunkResult
280298
.then(unwrapResult)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { SubMiddlewareBuilder } from './types'
2+
3+
export const build: SubMiddlewareBuilder = ({ api }) => {
4+
return (mwApi) => {
5+
let initialized = false
6+
return (next) => (action) => {
7+
if (!initialized) {
8+
initialized = true
9+
// dispatch before any other action
10+
mwApi.dispatch(api.internalActions.middlewareRegistered())
11+
}
12+
13+
const result = next(action)
14+
15+
if (api.util.resetApiState.match(action)) {
16+
// dispatch after api reset
17+
mwApi.dispatch(api.internalActions.middlewareRegistered())
18+
}
19+
20+
return result
21+
}
22+
}
23+
}

src/query/core/buildMiddleware/index.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import { compose } from 'redux'
22

3-
import type {
4-
AnyAction,
5-
Middleware,
6-
ThunkDispatch} from '@reduxjs/toolkit';
7-
import {
8-
createAction
9-
} from '@reduxjs/toolkit'
3+
import type { AnyAction, Middleware, ThunkDispatch } from '@reduxjs/toolkit'
4+
import { createAction } from '@reduxjs/toolkit'
105

116
import type {
127
EndpointDefinitions,
@@ -21,6 +16,7 @@ import type { BuildMiddlewareInput } from './types'
2116
import { build as buildWindowEventHandling } from './windowEventHandling'
2217
import { build as buildCacheLifecycle } from './cacheLifecycle'
2318
import { build as buildQueryLifecycle } from './queryLifecycle'
19+
import { build as buildDevMiddleware } from './devMiddleware'
2420

2521
export function buildMiddleware<
2622
Definitions extends EndpointDefinitions,
@@ -35,6 +31,7 @@ export function buildMiddleware<
3531
}
3632

3733
const middlewares = [
34+
buildDevMiddleware,
3835
buildCacheCollection,
3936
buildInvalidationByTags,
4037
buildPolling,
@@ -56,8 +53,15 @@ export function buildMiddleware<
5653
RootState<Definitions, string, ReducerPath>,
5754
ThunkDispatch<any, any, AnyAction>
5855
> = (mwApi) => (next) => {
59-
const chain = middlewares.map((middleware) => middleware(mwApi))
60-
return compose<typeof next>(...chain)(next)
56+
const applied = compose<typeof next>(
57+
...middlewares.map((middleware) => middleware(mwApi))
58+
)(next)
59+
return (action) => {
60+
if (mwApi.getState()[reducerPath]) {
61+
return applied(action)
62+
}
63+
return next(action)
64+
}
6165
}
6266

6367
return { middleware, actions }

src/query/core/buildSelectors.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,17 @@ export function buildSelectors<
133133
}
134134

135135
function selectInternalState(rootState: RootState) {
136-
return rootState[reducerPath]
136+
const state = rootState[reducerPath]
137+
if (process.env.NODE_ENV !== 'production') {
138+
if (!state) {
139+
if ((selectInternalState as any).triggered) return state
140+
;(selectInternalState as any).triggered = true
141+
console.error(
142+
`Error: No data found at \`state.${reducerPath}\`. Did you forget to add the reducer to the store?`
143+
)
144+
}
145+
}
146+
return state
137147
}
138148

139149
function buildQuerySelector(
@@ -146,7 +156,7 @@ export function buildSelectors<
146156
(internalState) =>
147157
(queryArgs === skipToken
148158
? undefined
149-
: internalState.queries[
159+
: internalState?.queries?.[
150160
serializeQueryArgs({
151161
queryArgs,
152162
endpointDefinition,
@@ -168,7 +178,7 @@ export function buildSelectors<
168178
(internalState) =>
169179
(mutationId === skipToken
170180
? undefined
171-
: internalState.mutations[mutationId]) ?? defaultMutationSubState
181+
: internalState?.mutations?.[mutationId]) ?? defaultMutationSubState
172182
)
173183
return createSelector(selectMutationSubstate, withRequestFlags)
174184
}

src/query/core/buildSlice.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,10 @@ export function buildSlice({
8585
mutationThunk: AsyncThunk<ThunkResult, MutationThunkArg, {}>
8686
context: ApiContext<EndpointDefinitions>
8787
assertTagType: AssertTagTypes
88-
config: Omit<ConfigState<string>, 'online' | 'focused'>
88+
config: Omit<
89+
ConfigState<string>,
90+
'online' | 'focused' | 'middlewareRegistered'
91+
>
8992
}) {
9093
const resetApiState = createAction(`${reducerPath}/resetApiState`)
9194
const querySlice = createSlice({
@@ -168,7 +171,7 @@ export function buildSlice({
168171
name: `${reducerPath}/mutations`,
169172
initialState: initialState as MutationState<any>,
170173
reducers: {
171-
unsubscribeResult(
174+
unsubscribeMutationResult(
172175
draft,
173176
action: PayloadAction<MutationSubstateIdentifier>
174177
) {
@@ -286,7 +289,7 @@ export function buildSlice({
286289
draft[queryCacheKey]![requestId] = options
287290
}
288291
},
289-
unsubscribeResult(
292+
unsubscribeQueryResult(
290293
draft,
291294
{
292295
payload: { queryCacheKey, requestId },
@@ -331,9 +334,14 @@ export function buildSlice({
331334
initialState: {
332335
online: isOnline(),
333336
focused: isDocumentVisible(),
337+
middlewareRegistered: false,
334338
...config,
335339
} as ConfigState<string>,
336-
reducers: {},
340+
reducers: {
341+
middlewareRegistered(state) {
342+
state.middlewareRegistered = true
343+
},
344+
},
337345
extraReducers: (builder) => {
338346
builder
339347
.addCase(onOnline, (state) => {
@@ -365,12 +373,10 @@ export function buildSlice({
365373
combinedReducer(resetApiState.match(action) ? undefined : state, action)
366374

367375
const actions = {
368-
updateSubscriptionOptions:
369-
subscriptionSlice.actions.updateSubscriptionOptions,
370-
queryResultPatched: querySlice.actions.queryResultPatched,
371-
removeQueryResult: querySlice.actions.removeQueryResult,
372-
unsubscribeQueryResult: subscriptionSlice.actions.unsubscribeResult,
373-
unsubscribeMutationResult: mutationSlice.actions.unsubscribeResult,
376+
...configSlice.actions,
377+
...querySlice.actions,
378+
...subscriptionSlice.actions,
379+
...mutationSlice.actions,
374380
resetApiState,
375381
}
376382

src/query/tests/buildHooks.test.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
actionsReducer,
1313
expectExactType,
1414
expectType,
15-
matchSequence,
1615
setupApiStore,
1716
useRenderCounter,
1817
waitMs,
@@ -1268,16 +1267,15 @@ describe('hooks tests', () => {
12681267
)
12691268

12701269
const { checkSession, login } = api.endpoints
1271-
const completeSequence = [
1270+
expect(storeRef.store.getState().actions).toMatchSequence(
1271+
api.internalActions.middlewareRegistered.match,
12721272
checkSession.matchPending,
12731273
checkSession.matchRejected,
12741274
login.matchPending,
12751275
login.matchFulfilled,
12761276
checkSession.matchPending,
1277-
checkSession.matchFulfilled,
1278-
]
1279-
1280-
matchSequence(storeRef.store.getState().actions, ...completeSequence)
1277+
checkSession.matchFulfilled
1278+
)
12811279
})
12821280
})
12831281
})
@@ -1878,15 +1876,14 @@ describe('hooks with createApi defaults set', () => {
18781876

18791877
const { increment } = api.endpoints
18801878

1881-
const completeSequence = [
1879+
expect(storeRef.store.getState().actions).toMatchSequence(
1880+
api.internalActions.middlewareRegistered.match,
18821881
increment.matchPending,
18831882
increment.matchFulfilled,
18841883
api.internalActions.unsubscribeMutationResult.match,
18851884
increment.matchPending,
1886-
increment.matchFulfilled,
1887-
]
1888-
1889-
matchSequence(storeRef.store.getState().actions, ...completeSequence)
1885+
increment.matchFulfilled
1886+
)
18901887
})
18911888

18921889
it('causes rerenders when only selected data changes', async () => {

src/query/tests/buildMiddleware.test.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createApi } from '@reduxjs/toolkit/query'
2-
import { actionsReducer, matchSequence, setupApiStore, waitMs } from './helpers'
2+
import { actionsReducer, setupApiStore, waitMs } from './helpers'
33

44
const baseQuery = (args?: any) => ({ data: args })
55
const api = createApi({
@@ -28,8 +28,8 @@ const storeRef = setupApiStore(api, {
2828

2929
it('invalidates the specified tags', async () => {
3030
await storeRef.store.dispatch(getBanana.initiate(1))
31-
matchSequence(
32-
storeRef.store.getState().actions,
31+
expect(storeRef.store.getState().actions).toMatchSequence(
32+
api.internalActions.middlewareRegistered.match,
3333
getBanana.matchPending,
3434
getBanana.matchFulfilled
3535
)
@@ -40,21 +40,21 @@ it('invalidates the specified tags', async () => {
4040
await waitMs(20)
4141

4242
const firstSequence = [
43+
api.internalActions.middlewareRegistered.match,
4344
getBanana.matchPending,
4445
getBanana.matchFulfilled,
4546
api.util.invalidateTags.match,
4647
getBanana.matchPending,
4748
getBanana.matchFulfilled,
4849
]
49-
matchSequence(storeRef.store.getState().actions, ...firstSequence)
50+
expect(storeRef.store.getState().actions).toMatchSequence(...firstSequence)
5051

5152
await storeRef.store.dispatch(getBread.initiate(1))
5253
await storeRef.store.dispatch(api.util.invalidateTags([{ type: 'Bread' }]))
5354

5455
await waitMs(20)
5556

56-
matchSequence(
57-
storeRef.store.getState().actions,
57+
expect(storeRef.store.getState().actions).toMatchSequence(
5858
...firstSequence,
5959
getBread.matchPending,
6060
getBread.matchFulfilled,

src/query/tests/buildSlice.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const authSlice = createSlice({
3030
const storeRef = setupApiStore(api, { auth: authSlice.reducer })
3131

3232
it('only resets the api state when resetApiState is dispatched', async () => {
33+
storeRef.store.dispatch({ type: 'unrelated' }) // trigger "registered middleware" into place
3334
const initialState = storeRef.store.getState()
3435

3536
await storeRef.store.dispatch(
@@ -41,6 +42,7 @@ it('only resets the api state when resetApiState is dispatched', async () => {
4142
config: {
4243
focused: true,
4344
keepUnusedDataFor: 60,
45+
middlewareRegistered: true,
4446
online: true,
4547
reducerPath: 'api',
4648
refetchOnFocus: false,

0 commit comments

Comments
 (0)