Skip to content

Commit

Permalink
Merge branch 'master' into pr/kyletsang/4638
Browse files Browse the repository at this point in the history
  • Loading branch information
EskiMojo14 committed Sep 24, 2024
2 parents 163f483 + 8178e7f commit 9f3a8b9
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 14 deletions.
1 change: 1 addition & 0 deletions docs/rtk-query/api/createApi.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export const { useGetPokemonByNameQuery } = pokemonApi
- `endpoint` - The name of the endpoint.
- `type` - Type of request (`query` or `mutation`).
- `forced` - Indicates if a query has been forced.
- `queryCacheKey`- The computed query cache key.
- `extraOptions` - The value of the optional `extraOptions` property provided for a given endpoint

#### baseQuery function signature
Expand Down
4 changes: 4 additions & 0 deletions packages/toolkit/src/query/baseQueryTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export interface BaseQueryApi {
* invalidated queries.
*/
forced?: boolean
/**
* Only available for queries: the cache key that was used to store the query result
*/
queryCacheKey?: string
}

export type QueryReturnValue<T = unknown, E = unknown, M = unknown> =
Expand Down
3 changes: 3 additions & 0 deletions packages/toolkit/src/query/core/apiState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ export function getRequestStatusFlags(status: QueryStatus): RequestStatusFlags {
} as any
}

/**
* @public
*/
export type SubscriptionOptions = {
/**
* How frequently to automatically re-fetch data (in milliseconds). Defaults to `0` (off).
Expand Down
1 change: 1 addition & 0 deletions packages/toolkit/src/query/core/buildThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ export function buildThunks<
type: arg.type,
forced:
arg.type === 'query' ? isForcedQuery(arg, getState()) : undefined,
queryCacheKey: arg.type === 'query' ? arg.queryCacheKey : undefined,
}

const forceQueryFn =
Expand Down
39 changes: 28 additions & 11 deletions packages/toolkit/src/query/fetchBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export type FetchBaseQueryArgs = {
BaseQueryApi,
'getState' | 'extra' | 'endpoint' | 'type' | 'forced'
>,
args: string | FetchArgs
args: string | FetchArgs,
) => MaybePromise<Headers | void>
fetchFn?: (
input: RequestInfo,
Expand Down Expand Up @@ -214,7 +214,7 @@ export function fetchBaseQuery({
)
}
return async (args, api) => {
const { signal, getState, extra, endpoint, forced, type } = api
const { getState, extra, endpoint, forced, type } = api
let meta: FetchBaseQueryMeta | undefined
let {
url,
Expand All @@ -225,6 +225,15 @@ export function fetchBaseQuery({
timeout = defaultTimeout,
...rest
} = typeof args == 'string' ? { url: args } : args

let abortController: AbortController | undefined,
signal = api.signal
if (timeout) {
abortController = new AbortController()
api.signal.addEventListener('abort', abortController.abort)
signal = abortController.signal
}

let config: RequestInit = {
...baseFetchOptions,
signal,
Expand All @@ -233,13 +242,17 @@ export function fetchBaseQuery({

headers = new Headers(stripUndefined(headers))
config.headers =
(await prepareHeaders(headers, {
getState,
extra,
endpoint,
forced,
type,
}, args)) || headers
(await prepareHeaders(
headers,
{
getState,
extra,
endpoint,
forced,
type,
},
args,
)) || headers

// Only set the content-type to json if appropriate. Will not be true for FormData, ArrayBuffer, Blob, etc.
const isJsonifiable = (body: any) =>
Expand Down Expand Up @@ -273,10 +286,10 @@ export function fetchBaseQuery({
let response,
timedOut = false,
timeoutId =
timeout &&
abortController &&
setTimeout(() => {
timedOut = true
api.abort()
abortController!.abort()
}, timeout)
try {
response = await fetchFn(request)
Expand All @@ -290,6 +303,10 @@ export function fetchBaseQuery({
}
} finally {
if (timeoutId) clearTimeout(timeoutId)
abortController?.signal.removeEventListener(
'abort',
abortController.abort,
)
}
const responseClone = response.clone()

Expand Down
1 change: 1 addition & 0 deletions packages/toolkit/src/query/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type {
BaseQueryApi,
BaseQueryEnhancer,
BaseQueryFn,
QueryReturnValue
} from './baseQueryTypes'
export type {
BaseEndpointDefinition,
Expand Down
76 changes: 76 additions & 0 deletions packages/toolkit/src/query/react/buildHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,9 @@ export type TypedUseQueryState<
QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>
>

/**
* @internal
*/
export type UseQueryStateOptions<
D extends QueryDefinition<any, any, any, any>,
R extends Record<string, any>,
Expand Down Expand Up @@ -427,6 +430,79 @@ export type UseQueryStateOptions<
selectFromResult?: QueryStateSelector<R, D>
}

/**
* Provides a way to define a "pre-typed" version of
* {@linkcode UseQueryStateOptions} with specific options for a given query.
* This is particularly useful for setting default query behaviors such as
* refetching strategies, which can be overridden as needed.
*
* @example
* <caption>#### __Create a `useQuery` hook with default options__</caption>
*
* ```ts
* import type {
* SubscriptionOptions,
* TypedUseQueryStateOptions,
* } from '@reduxjs/toolkit/query/react'
* import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
*
* type Post = {
* id: number
* name: string
* }
*
* const api = createApi({
* baseQuery: fetchBaseQuery({ baseUrl: '/' }),
* tagTypes: ['Post'],
* endpoints: (build) => ({
* getPosts: build.query<Post[], void>({
* query: () => 'posts',
* }),
* }),
* })
*
* const { useGetPostsQuery } = api
*
* export const useGetPostsQueryWithDefaults = <
* SelectedResult extends Record<string, any>,
* >(
* overrideOptions: TypedUseQueryStateOptions<
* Post[],
* void,
* ReturnType<typeof fetchBaseQuery>,
* SelectedResult
* > &
* SubscriptionOptions,
* ) =>
* useGetPostsQuery(undefined, {
* // Insert default options here
*
* refetchOnMountOrArgChange: true,
* refetchOnFocus: true,
* ...overrideOptions,
* })
* ```
*
* @template ResultType - The type of the result `data` returned by the query.
* @template QueryArg - The type of the argument passed into the query.
* @template BaseQuery - The type of the base query function being used.
* @template SelectedResult - The type of the selected result returned by the __`selectFromResult`__ function.
*
* @since 2.7.8
* @public
*/
export type TypedUseQueryStateOptions<
ResultType,
QueryArg,
BaseQuery extends BaseQueryFn,
SelectedResult extends Record<string, any> = UseQueryStateDefaultResult<
QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>
>,
> = UseQueryStateOptions<
QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>,
SelectedResult
>

export type UseQueryStateResult<
_ extends QueryDefinition<any, any, any, any>,
R,
Expand Down
1 change: 1 addition & 0 deletions packages/toolkit/src/query/react/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type {
TypedUseQuery,
TypedUseQuerySubscription,
TypedUseLazyQuerySubscription,
TypedUseQueryStateOptions,
} from './buildHooks'
export { UNINITIALIZED_VALUE } from './constants'
export { createApi, reactHooksModule, reactHooksModuleName }
56 changes: 55 additions & 1 deletion packages/toolkit/src/query/tests/buildInitiate.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { setupApiStore } from '../../tests/utils/helpers'
import { setupApiStore } from '@internal/tests/utils/helpers'
import { createApi } from '../core'
import type { SubscriptionSelectors } from '../core/buildMiddleware/types'
import { fakeBaseQuery } from '../fakeBaseQuery'
Expand Down Expand Up @@ -119,3 +119,57 @@ describe('calling initiate without a cache entry, with subscribe: false still re
).toBe(false)
})
})

describe('calling initiate should have resulting queryCacheKey match baseQuery queryCacheKey', () => {
const baseQuery = vi.fn(() => ({ data: 'success' }))
function getNewApi() {
return createApi({
baseQuery,
endpoints: (build) => ({
query: build.query<void, { arg1: string; arg2: string }>({
query: (args) => `queryUrl/${args.arg1}/${args.arg2}`,
}),
mutation: build.mutation<void, { arg1: string; arg2: string }>({
query: () => 'mutationUrl',
}),
}),
})
}
let api = getNewApi()
beforeEach(() => {
baseQuery.mockClear()
api = getNewApi()
})

test('should be a string and matching on queries', () => {
const { store: storeApi } = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
const promise = storeApi.dispatch(
api.endpoints.query.initiate({ arg2: 'secondArg', arg1: 'firstArg' }),
)
expect(baseQuery).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
queryCacheKey: promise.queryCacheKey,
}),
undefined,
)
})

test('should be undefined and matching on mutations', () => {
const { store: storeApi } = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeApi.dispatch(
api.endpoints.mutation.initiate({ arg2: 'secondArg', arg1: 'firstArg' }),
)
expect(baseQuery).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
queryCacheKey: undefined,
}),
undefined,
)
})
})
58 changes: 56 additions & 2 deletions packages/toolkit/src/query/tests/createApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ describe('endpoint definition typings', () => {
getState: expect.any(Function),
signal: expect.any(Object),
type: expect.any(String),
queryCacheKey: expect.any(String),
}
beforeEach(() => {
baseQuery.mockClear()
Expand Down Expand Up @@ -355,6 +356,7 @@ describe('endpoint definition typings', () => {
abort: expect.any(Function),
forced: expect.any(Boolean),
type: expect.any(String),
queryCacheKey: expect.any(String),
},
undefined,
],
Expand All @@ -368,6 +370,7 @@ describe('endpoint definition typings', () => {
abort: expect.any(Function),
forced: expect.any(Boolean),
type: expect.any(String),
queryCacheKey: expect.any(String),
},
undefined,
],
Expand Down Expand Up @@ -499,8 +502,24 @@ describe('endpoint definition typings', () => {
expect(baseQuery.mock.calls).toEqual([
['modified1', commonBaseQueryApi, undefined],
['modified2', commonBaseQueryApi, undefined],
['modified1', { ...commonBaseQueryApi, forced: undefined }, undefined],
['modified2', { ...commonBaseQueryApi, forced: undefined }, undefined],
[
'modified1',
{
...commonBaseQueryApi,
forced: undefined,
queryCacheKey: undefined,
},
undefined,
],
[
'modified2',
{
...commonBaseQueryApi,
forced: undefined,
queryCacheKey: undefined,
},
undefined,
],
])
})

Expand Down Expand Up @@ -1128,3 +1147,38 @@ describe('custom serializeQueryArgs per endpoint', () => {
})
})
})

describe('timeout behavior', () => {
test('triggers TIMEOUT_ERROR', async () => {
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com', timeout: 5 }),
endpoints: (build) => ({
query: build.query<unknown, void>({
query: () => '/success',
}),
}),
})

const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})

server.use(
http.get(
'https://example.com/success',
async () => {
await delay(10)
return HttpResponse.json({ value: 'failed' }, { status: 500 })
},
{ once: true },
),
)

const result = await storeRef.store.dispatch(api.endpoints.query.initiate())

expect(result?.error).toEqual({
status: 'TIMEOUT_ERROR',
error: expect.stringMatching(/^AbortError:/),
})
})
})
Loading

0 comments on commit 9f3a8b9

Please sign in to comment.