Skip to content

Commit 9f3a8b9

Browse files
committed
Merge branch 'master' into pr/kyletsang/4638
2 parents 163f483 + 8178e7f commit 9f3a8b9

File tree

11 files changed

+246
-14
lines changed

11 files changed

+246
-14
lines changed

docs/rtk-query/api/createApi.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export const { useGetPokemonByNameQuery } = pokemonApi
9696
- `endpoint` - The name of the endpoint.
9797
- `type` - Type of request (`query` or `mutation`).
9898
- `forced` - Indicates if a query has been forced.
99+
- `queryCacheKey`- The computed query cache key.
99100
- `extraOptions` - The value of the optional `extraOptions` property provided for a given endpoint
100101

101102
#### baseQuery function signature

packages/toolkit/src/query/baseQueryTypes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export interface BaseQueryApi {
1818
* invalidated queries.
1919
*/
2020
forced?: boolean
21+
/**
22+
* Only available for queries: the cache key that was used to store the query result
23+
*/
24+
queryCacheKey?: string
2125
}
2226

2327
export type QueryReturnValue<T = unknown, E = unknown, M = unknown> =

packages/toolkit/src/query/core/apiState.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ export function getRequestStatusFlags(status: QueryStatus): RequestStatusFlags {
7878
} as any
7979
}
8080

81+
/**
82+
* @public
83+
*/
8184
export type SubscriptionOptions = {
8285
/**
8386
* How frequently to automatically re-fetch data (in milliseconds). Defaults to `0` (off).

packages/toolkit/src/query/core/buildThunks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ export function buildThunks<
381381
type: arg.type,
382382
forced:
383383
arg.type === 'query' ? isForcedQuery(arg, getState()) : undefined,
384+
queryCacheKey: arg.type === 'query' ? arg.queryCacheKey : undefined,
384385
}
385386

386387
const forceQueryFn =

packages/toolkit/src/query/fetchBaseQuery.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export type FetchBaseQueryArgs = {
113113
BaseQueryApi,
114114
'getState' | 'extra' | 'endpoint' | 'type' | 'forced'
115115
>,
116-
args: string | FetchArgs
116+
args: string | FetchArgs,
117117
) => MaybePromise<Headers | void>
118118
fetchFn?: (
119119
input: RequestInfo,
@@ -214,7 +214,7 @@ export function fetchBaseQuery({
214214
)
215215
}
216216
return async (args, api) => {
217-
const { signal, getState, extra, endpoint, forced, type } = api
217+
const { getState, extra, endpoint, forced, type } = api
218218
let meta: FetchBaseQueryMeta | undefined
219219
let {
220220
url,
@@ -225,6 +225,15 @@ export function fetchBaseQuery({
225225
timeout = defaultTimeout,
226226
...rest
227227
} = typeof args == 'string' ? { url: args } : args
228+
229+
let abortController: AbortController | undefined,
230+
signal = api.signal
231+
if (timeout) {
232+
abortController = new AbortController()
233+
api.signal.addEventListener('abort', abortController.abort)
234+
signal = abortController.signal
235+
}
236+
228237
let config: RequestInit = {
229238
...baseFetchOptions,
230239
signal,
@@ -233,13 +242,17 @@ export function fetchBaseQuery({
233242

234243
headers = new Headers(stripUndefined(headers))
235244
config.headers =
236-
(await prepareHeaders(headers, {
237-
getState,
238-
extra,
239-
endpoint,
240-
forced,
241-
type,
242-
}, args)) || headers
245+
(await prepareHeaders(
246+
headers,
247+
{
248+
getState,
249+
extra,
250+
endpoint,
251+
forced,
252+
type,
253+
},
254+
args,
255+
)) || headers
243256

244257
// Only set the content-type to json if appropriate. Will not be true for FormData, ArrayBuffer, Blob, etc.
245258
const isJsonifiable = (body: any) =>
@@ -273,10 +286,10 @@ export function fetchBaseQuery({
273286
let response,
274287
timedOut = false,
275288
timeoutId =
276-
timeout &&
289+
abortController &&
277290
setTimeout(() => {
278291
timedOut = true
279-
api.abort()
292+
abortController!.abort()
280293
}, timeout)
281294
try {
282295
response = await fetchFn(request)
@@ -290,6 +303,10 @@ export function fetchBaseQuery({
290303
}
291304
} finally {
292305
if (timeoutId) clearTimeout(timeoutId)
306+
abortController?.signal.removeEventListener(
307+
'abort',
308+
abortController.abort,
309+
)
293310
}
294311
const responseClone = response.clone()
295312

packages/toolkit/src/query/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type {
1717
BaseQueryApi,
1818
BaseQueryEnhancer,
1919
BaseQueryFn,
20+
QueryReturnValue
2021
} from './baseQueryTypes'
2122
export type {
2223
BaseEndpointDefinition,

packages/toolkit/src/query/react/buildHooks.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,9 @@ export type TypedUseQueryState<
357357
QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>
358358
>
359359

360+
/**
361+
* @internal
362+
*/
360363
export type UseQueryStateOptions<
361364
D extends QueryDefinition<any, any, any, any>,
362365
R extends Record<string, any>,
@@ -427,6 +430,79 @@ export type UseQueryStateOptions<
427430
selectFromResult?: QueryStateSelector<R, D>
428431
}
429432

433+
/**
434+
* Provides a way to define a "pre-typed" version of
435+
* {@linkcode UseQueryStateOptions} with specific options for a given query.
436+
* This is particularly useful for setting default query behaviors such as
437+
* refetching strategies, which can be overridden as needed.
438+
*
439+
* @example
440+
* <caption>#### __Create a `useQuery` hook with default options__</caption>
441+
*
442+
* ```ts
443+
* import type {
444+
* SubscriptionOptions,
445+
* TypedUseQueryStateOptions,
446+
* } from '@reduxjs/toolkit/query/react'
447+
* import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
448+
*
449+
* type Post = {
450+
* id: number
451+
* name: string
452+
* }
453+
*
454+
* const api = createApi({
455+
* baseQuery: fetchBaseQuery({ baseUrl: '/' }),
456+
* tagTypes: ['Post'],
457+
* endpoints: (build) => ({
458+
* getPosts: build.query<Post[], void>({
459+
* query: () => 'posts',
460+
* }),
461+
* }),
462+
* })
463+
*
464+
* const { useGetPostsQuery } = api
465+
*
466+
* export const useGetPostsQueryWithDefaults = <
467+
* SelectedResult extends Record<string, any>,
468+
* >(
469+
* overrideOptions: TypedUseQueryStateOptions<
470+
* Post[],
471+
* void,
472+
* ReturnType<typeof fetchBaseQuery>,
473+
* SelectedResult
474+
* > &
475+
* SubscriptionOptions,
476+
* ) =>
477+
* useGetPostsQuery(undefined, {
478+
* // Insert default options here
479+
*
480+
* refetchOnMountOrArgChange: true,
481+
* refetchOnFocus: true,
482+
* ...overrideOptions,
483+
* })
484+
* ```
485+
*
486+
* @template ResultType - The type of the result `data` returned by the query.
487+
* @template QueryArg - The type of the argument passed into the query.
488+
* @template BaseQuery - The type of the base query function being used.
489+
* @template SelectedResult - The type of the selected result returned by the __`selectFromResult`__ function.
490+
*
491+
* @since 2.7.8
492+
* @public
493+
*/
494+
export type TypedUseQueryStateOptions<
495+
ResultType,
496+
QueryArg,
497+
BaseQuery extends BaseQueryFn,
498+
SelectedResult extends Record<string, any> = UseQueryStateDefaultResult<
499+
QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>
500+
>,
501+
> = UseQueryStateOptions<
502+
QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>,
503+
SelectedResult
504+
>
505+
430506
export type UseQueryStateResult<
431507
_ extends QueryDefinition<any, any, any, any>,
432508
R,

packages/toolkit/src/query/react/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export type {
2626
TypedUseQuery,
2727
TypedUseQuerySubscription,
2828
TypedUseLazyQuerySubscription,
29+
TypedUseQueryStateOptions,
2930
} from './buildHooks'
3031
export { UNINITIALIZED_VALUE } from './constants'
3132
export { createApi, reactHooksModule, reactHooksModuleName }

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

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { setupApiStore } from '../../tests/utils/helpers'
1+
import { setupApiStore } from '@internal/tests/utils/helpers'
22
import { createApi } from '../core'
33
import type { SubscriptionSelectors } from '../core/buildMiddleware/types'
44
import { fakeBaseQuery } from '../fakeBaseQuery'
@@ -119,3 +119,57 @@ describe('calling initiate without a cache entry, with subscribe: false still re
119119
).toBe(false)
120120
})
121121
})
122+
123+
describe('calling initiate should have resulting queryCacheKey match baseQuery queryCacheKey', () => {
124+
const baseQuery = vi.fn(() => ({ data: 'success' }))
125+
function getNewApi() {
126+
return createApi({
127+
baseQuery,
128+
endpoints: (build) => ({
129+
query: build.query<void, { arg1: string; arg2: string }>({
130+
query: (args) => `queryUrl/${args.arg1}/${args.arg2}`,
131+
}),
132+
mutation: build.mutation<void, { arg1: string; arg2: string }>({
133+
query: () => 'mutationUrl',
134+
}),
135+
}),
136+
})
137+
}
138+
let api = getNewApi()
139+
beforeEach(() => {
140+
baseQuery.mockClear()
141+
api = getNewApi()
142+
})
143+
144+
test('should be a string and matching on queries', () => {
145+
const { store: storeApi } = setupApiStore(api, undefined, {
146+
withoutTestLifecycles: true,
147+
})
148+
const promise = storeApi.dispatch(
149+
api.endpoints.query.initiate({ arg2: 'secondArg', arg1: 'firstArg' }),
150+
)
151+
expect(baseQuery).toHaveBeenCalledWith(
152+
expect.any(String),
153+
expect.objectContaining({
154+
queryCacheKey: promise.queryCacheKey,
155+
}),
156+
undefined,
157+
)
158+
})
159+
160+
test('should be undefined and matching on mutations', () => {
161+
const { store: storeApi } = setupApiStore(api, undefined, {
162+
withoutTestLifecycles: true,
163+
})
164+
storeApi.dispatch(
165+
api.endpoints.mutation.initiate({ arg2: 'secondArg', arg1: 'firstArg' }),
166+
)
167+
expect(baseQuery).toHaveBeenCalledWith(
168+
expect.any(String),
169+
expect.objectContaining({
170+
queryCacheKey: undefined,
171+
}),
172+
undefined,
173+
)
174+
})
175+
})

packages/toolkit/src/query/tests/createApi.test.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ describe('endpoint definition typings', () => {
314314
getState: expect.any(Function),
315315
signal: expect.any(Object),
316316
type: expect.any(String),
317+
queryCacheKey: expect.any(String),
317318
}
318319
beforeEach(() => {
319320
baseQuery.mockClear()
@@ -355,6 +356,7 @@ describe('endpoint definition typings', () => {
355356
abort: expect.any(Function),
356357
forced: expect.any(Boolean),
357358
type: expect.any(String),
359+
queryCacheKey: expect.any(String),
358360
},
359361
undefined,
360362
],
@@ -368,6 +370,7 @@ describe('endpoint definition typings', () => {
368370
abort: expect.any(Function),
369371
forced: expect.any(Boolean),
370372
type: expect.any(String),
373+
queryCacheKey: expect.any(String),
371374
},
372375
undefined,
373376
],
@@ -499,8 +502,24 @@ describe('endpoint definition typings', () => {
499502
expect(baseQuery.mock.calls).toEqual([
500503
['modified1', commonBaseQueryApi, undefined],
501504
['modified2', commonBaseQueryApi, undefined],
502-
['modified1', { ...commonBaseQueryApi, forced: undefined }, undefined],
503-
['modified2', { ...commonBaseQueryApi, forced: undefined }, undefined],
505+
[
506+
'modified1',
507+
{
508+
...commonBaseQueryApi,
509+
forced: undefined,
510+
queryCacheKey: undefined,
511+
},
512+
undefined,
513+
],
514+
[
515+
'modified2',
516+
{
517+
...commonBaseQueryApi,
518+
forced: undefined,
519+
queryCacheKey: undefined,
520+
},
521+
undefined,
522+
],
504523
])
505524
})
506525

@@ -1128,3 +1147,38 @@ describe('custom serializeQueryArgs per endpoint', () => {
11281147
})
11291148
})
11301149
})
1150+
1151+
describe('timeout behavior', () => {
1152+
test('triggers TIMEOUT_ERROR', async () => {
1153+
const api = createApi({
1154+
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com', timeout: 5 }),
1155+
endpoints: (build) => ({
1156+
query: build.query<unknown, void>({
1157+
query: () => '/success',
1158+
}),
1159+
}),
1160+
})
1161+
1162+
const storeRef = setupApiStore(api, undefined, {
1163+
withoutTestLifecycles: true,
1164+
})
1165+
1166+
server.use(
1167+
http.get(
1168+
'https://example.com/success',
1169+
async () => {
1170+
await delay(10)
1171+
return HttpResponse.json({ value: 'failed' }, { status: 500 })
1172+
},
1173+
{ once: true },
1174+
),
1175+
)
1176+
1177+
const result = await storeRef.store.dispatch(api.endpoints.query.initiate())
1178+
1179+
expect(result?.error).toEqual({
1180+
status: 'TIMEOUT_ERROR',
1181+
error: expect.stringMatching(/^AbortError:/),
1182+
})
1183+
})
1184+
})

0 commit comments

Comments
 (0)