UNPKG

39.3 kBPlain TextView Raw
1import type {
2 Selector,
3 ThunkAction,
4 ThunkDispatch,
5 UnknownAction,
6} from '@reduxjs/toolkit'
7import type {
8 Api,
9 ApiContext,
10 ApiEndpointMutation,
11 ApiEndpointQuery,
12 CoreModule,
13 EndpointDefinitions,
14 MutationActionCreatorResult,
15 MutationDefinition,
16 MutationResultSelectorResult,
17 PrefetchOptions,
18 QueryActionCreatorResult,
19 QueryArgFrom,
20 QueryDefinition,
21 QueryKeys,
22 QueryResultSelectorResult,
23 QuerySubState,
24 ResultTypeFrom,
25 RootState,
26 SerializeQueryArgs,
27 SkipToken,
28 SubscriptionOptions,
29 TSHelpersId,
30 TSHelpersNoInfer,
31 TSHelpersOverride,
32} from '@reduxjs/toolkit/query'
33import { QueryStatus, skipToken } from '@reduxjs/toolkit/query'
34import type { DependencyList } from 'react'
35import {
36 useCallback,
37 useDebugValue,
38 useEffect,
39 useLayoutEffect,
40 useMemo,
41 useRef,
42 useState,
43} from 'react'
44
45import { shallowEqual } from 'react-redux'
46import type { BaseQueryFn } from '../baseQueryTypes'
47import type { SubscriptionSelectors } from '../core/buildMiddleware/types'
48import { defaultSerializeQueryArgs } from '../defaultSerializeQueryArgs'
49import type { UninitializedValue } from './constants'
50import { UNINITIALIZED_VALUE } from './constants'
51import type { ReactHooksModuleOptions } from './module'
52import { useStableQueryArgs } from './useSerializedStableValue'
53import { useShallowStableValue } from './useShallowStableValue'
54
55// Copy-pasted from React-Redux
56export const useIsomorphicLayoutEffect =
57 typeof window !== 'undefined' &&
58 !!window.document &&
59 !!window.document.createElement
60 ? useLayoutEffect
61 : useEffect
62
63export interface QueryHooks<
64 Definition extends QueryDefinition<any, any, any, any, any>,
65> {
66 useQuery: UseQuery<Definition>
67 useLazyQuery: UseLazyQuery<Definition>
68 useQuerySubscription: UseQuerySubscription<Definition>
69 useLazyQuerySubscription: UseLazyQuerySubscription<Definition>
70 useQueryState: UseQueryState<Definition>
71}
72
73export interface MutationHooks<
74 Definition extends MutationDefinition<any, any, any, any, any>,
75> {
76 useMutation: UseMutation<Definition>
77}
78
79/**
80 * A React hook that automatically triggers fetches of data from an endpoint, 'subscribes' the component to the cached data, and reads the request status and cached data from the Redux store. The component will re-render as the loading status changes and the data becomes available.
81 *
82 * The query arg is used as a cache key. Changing the query arg will tell the hook to re-fetch the data if it does not exist in the cache already, and the hook will return the data for that query arg once it's available.
83 *
84 * This hook combines the functionality of both [`useQueryState`](#usequerystate) and [`useQuerySubscription`](#usequerysubscription) together, and is intended to be used in the majority of situations.
85 *
86 * #### Features
87 *
88 * - Automatically triggers requests to retrieve data based on the hook argument and whether cached data exists by default
89 * - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
90 * - Accepts polling/re-fetching options to trigger automatic re-fetches when the corresponding criteria is met
91 * - Returns the latest request status and cached data from the Redux store
92 * - Re-renders as the request status changes and data becomes available
93 */
94export type UseQuery<D extends QueryDefinition<any, any, any, any>> = <
95 R extends Record<string, any> = UseQueryStateDefaultResult<D>,
96>(
97 arg: QueryArgFrom<D> | SkipToken,
98 options?: UseQuerySubscriptionOptions & UseQueryStateOptions<D, R>,
99) => UseQueryHookResult<D, R>
100
101export type TypedUseQuery<
102 ResultType,
103 QueryArg,
104 BaseQuery extends BaseQueryFn,
105> = UseQuery<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>
106
107export type UseQueryHookResult<
108 D extends QueryDefinition<any, any, any, any>,
109 R = UseQueryStateDefaultResult<D>,
110> = UseQueryStateResult<D, R> & UseQuerySubscriptionResult<D>
111
112/**
113 * Helper type to manually type the result
114 * of the `useQuery` hook in userland code.
115 */
116export type TypedUseQueryHookResult<
117 ResultType,
118 QueryArg,
119 BaseQuery extends BaseQueryFn,
120 R = UseQueryStateDefaultResult<
121 QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>
122 >,
123> = TypedUseQueryStateResult<ResultType, QueryArg, BaseQuery, R> &
124 TypedUseQuerySubscriptionResult<ResultType, QueryArg, BaseQuery>
125
126interface UseQuerySubscriptionOptions extends SubscriptionOptions {
127 /**
128 * Prevents a query from automatically running.
129 *
130 * @remarks
131 * When `skip` is true (or `skipToken` is passed in as `arg`):
132 *
133 * - **If the query has cached data:**
134 * * The cached data **will not be used** on the initial load, and will ignore updates from any identical query until the `skip` condition is removed
135 * * The query will have a status of `uninitialized`
136 * * If `skip: false` is set after the initial load, the cached result will be used
137 * - **If the query does not have cached data:**
138 * * The query will have a status of `uninitialized`
139 * * The query will not exist in the state when viewed with the dev tools
140 * * The query will not automatically fetch on mount
141 * * The query will not automatically run when additional components with the same query are added that do run
142 *
143 * @example
144 * ```tsx
145 * // codeblock-meta no-transpile title="Skip example"
146 * const Pokemon = ({ name, skip }: { name: string; skip: boolean }) => {
147 * const { data, error, status } = useGetPokemonByNameQuery(name, {
148 * skip,
149 * });
150 *
151 * return (
152 * <div>
153 * {name} - {status}
154 * </div>
155 * );
156 * };
157 * ```
158 */
159 skip?: boolean
160 /**
161 * Defaults to `false`. This setting allows you to control whether if a cached result is already available, RTK Query will only serve a cached result, or if it should `refetch` when set to `true` or if an adequate amount of time has passed since the last successful query result.
162 * - `false` - Will not cause a query to be performed _unless_ it does not exist yet.
163 * - `true` - Will always refetch when a new subscriber to a query is added. Behaves the same as calling the `refetch` callback or passing `forceRefetch: true` in the action creator.
164 * - `number` - **Value is in seconds**. If a number is provided and there is an existing query in the cache, it will compare the current time vs the last fulfilled timestamp, and only refetch if enough time has elapsed.
165 *
166 * If you specify this option alongside `skip: true`, this **will not be evaluated** until `skip` is false.
167 */
168 refetchOnMountOrArgChange?: boolean | number
169}
170
171/**
172 * A React hook that automatically triggers fetches of data from an endpoint, and 'subscribes' the component to the cached data.
173 *
174 * The query arg is used as a cache key. Changing the query arg will tell the hook to re-fetch the data if it does not exist in the cache already.
175 *
176 * Note that this hook does not return a request status or cached data. For that use-case, see [`useQuery`](#usequery) or [`useQueryState`](#usequerystate).
177 *
178 * #### Features
179 *
180 * - Automatically triggers requests to retrieve data based on the hook argument and whether cached data exists by default
181 * - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
182 * - Accepts polling/re-fetching options to trigger automatic re-fetches when the corresponding criteria is met
183 */
184export type UseQuerySubscription<
185 D extends QueryDefinition<any, any, any, any>,
186> = (
187 arg: QueryArgFrom<D> | SkipToken,
188 options?: UseQuerySubscriptionOptions,
189) => UseQuerySubscriptionResult<D>
190
191export type TypedUseQuerySubscription<
192 ResultType,
193 QueryArg,
194 BaseQuery extends BaseQueryFn,
195> = UseQuerySubscription<
196 QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>
197>
198
199export type UseQuerySubscriptionResult<
200 D extends QueryDefinition<any, any, any, any>,
201> = Pick<QueryActionCreatorResult<D>, 'refetch'>
202
203/**
204 * Helper type to manually type the result
205 * of the `useQuerySubscription` hook in userland code.
206 */
207export type TypedUseQuerySubscriptionResult<
208 ResultType,
209 QueryArg,
210 BaseQuery extends BaseQueryFn,
211> = UseQuerySubscriptionResult<
212 QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>
213>
214
215export type UseLazyQueryLastPromiseInfo<
216 D extends QueryDefinition<any, any, any, any>,
217> = {
218 lastArg: QueryArgFrom<D>
219}
220
221/**
222 * A React hook similar to [`useQuery`](#usequery), but with manual control over when the data fetching occurs.
223 *
224 * This hook includes the functionality of [`useLazyQuerySubscription`](#uselazyquerysubscription).
225 *
226 * #### Features
227 *
228 * - Manual control over firing a request to retrieve data
229 * - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
230 * - Returns the latest request status and cached data from the Redux store
231 * - Re-renders as the request status changes and data becomes available
232 * - Accepts polling/re-fetching options to trigger automatic re-fetches when the corresponding criteria is met and the fetch has been manually called at least once
233 *
234 * #### Note
235 *
236 * When the trigger function returned from a LazyQuery is called, it always initiates a new request to the server even if there is cached data. Set `preferCacheValue`(the second argument to the function) as `true` if you want it to immediately return a cached value if one exists.
237 */
238export type UseLazyQuery<D extends QueryDefinition<any, any, any, any>> = <
239 R extends Record<string, any> = UseQueryStateDefaultResult<D>,
240>(
241 options?: SubscriptionOptions & Omit<UseQueryStateOptions<D, R>, 'skip'>,
242) => [
243 LazyQueryTrigger<D>,
244 UseQueryStateResult<D, R>,
245 UseLazyQueryLastPromiseInfo<D>,
246]
247
248export type TypedUseLazyQuery<
249 ResultType,
250 QueryArg,
251 BaseQuery extends BaseQueryFn,
252> = UseLazyQuery<
253 QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>
254>
255
256export type LazyQueryTrigger<D extends QueryDefinition<any, any, any, any>> = {
257 /**
258 * Triggers a lazy query.
259 *
260 * By default, this will start a new request even if there is already a value in the cache.
261 * If you want to use the cache value and only start a request if there is no cache value, set the second argument to `true`.
262 *
263 * @remarks
264 * If you need to access the error or success payload immediately after a lazy query, you can chain .unwrap().
265 *
266 * @example
267 * ```ts
268 * // codeblock-meta title="Using .unwrap with async await"
269 * try {
270 * const payload = await getUserById(1).unwrap();
271 * console.log('fulfilled', payload)
272 * } catch (error) {
273 * console.error('rejected', error);
274 * }
275 * ```
276 */
277 (
278 arg: QueryArgFrom<D>,
279 preferCacheValue?: boolean,
280 ): QueryActionCreatorResult<D>
281}
282
283export type TypedLazyQueryTrigger<
284 ResultType,
285 QueryArg,
286 BaseQuery extends BaseQueryFn,
287> = LazyQueryTrigger<
288 QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>
289>
290
291/**
292 * A React hook similar to [`useQuerySubscription`](#usequerysubscription), but with manual control over when the data fetching occurs.
293 *
294 * Note that this hook does not return a request status or cached data. For that use-case, see [`useLazyQuery`](#uselazyquery).
295 *
296 * #### Features
297 *
298 * - Manual control over firing a request to retrieve data
299 * - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
300 * - Accepts polling/re-fetching options to trigger automatic re-fetches when the corresponding criteria is met and the fetch has been manually called at least once
301 */
302export type UseLazyQuerySubscription<
303 D extends QueryDefinition<any, any, any, any>,
304> = (
305 options?: SubscriptionOptions,
306) => readonly [LazyQueryTrigger<D>, QueryArgFrom<D> | UninitializedValue]
307
308export type TypedUseLazyQuerySubscription<
309 ResultType,
310 QueryArg,
311 BaseQuery extends BaseQueryFn,
312> = UseLazyQuerySubscription<
313 QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>
314>
315
316export type QueryStateSelector<
317 R extends Record<string, any>,
318 D extends QueryDefinition<any, any, any, any>,
319> = (state: UseQueryStateDefaultResult<D>) => R
320
321/**
322 * A React hook that reads the request status and cached data from the Redux store. The component will re-render as the loading status changes and the data becomes available.
323 *
324 * Note that this hook does not trigger fetching new data. For that use-case, see [`useQuery`](#usequery) or [`useQuerySubscription`](#usequerysubscription).
325 *
326 * #### Features
327 *
328 * - Returns the latest request status and cached data from the Redux store
329 * - Re-renders as the request status changes and data becomes available
330 */
331export type UseQueryState<D extends QueryDefinition<any, any, any, any>> = <
332 R extends Record<string, any> = UseQueryStateDefaultResult<D>,
333>(
334 arg: QueryArgFrom<D> | SkipToken,
335 options?: UseQueryStateOptions<D, R>,
336) => UseQueryStateResult<D, R>
337
338export type TypedUseQueryState<
339 ResultType,
340 QueryArg,
341 BaseQuery extends BaseQueryFn,
342> = UseQueryState<
343 QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>
344>
345
346export type UseQueryStateOptions<
347 D extends QueryDefinition<any, any, any, any>,
348 R extends Record<string, any>,
349> = {
350 /**
351 * Prevents a query from automatically running.
352 *
353 * @remarks
354 * When skip is true:
355 *
356 * - **If the query has cached data:**
357 * * The cached data **will not be used** on the initial load, and will ignore updates from any identical query until the `skip` condition is removed
358 * * The query will have a status of `uninitialized`
359 * * If `skip: false` is set after skipping the initial load, the cached result will be used
360 * - **If the query does not have cached data:**
361 * * The query will have a status of `uninitialized`
362 * * The query will not exist in the state when viewed with the dev tools
363 * * The query will not automatically fetch on mount
364 * * The query will not automatically run when additional components with the same query are added that do run
365 *
366 * @example
367 * ```ts
368 * // codeblock-meta title="Skip example"
369 * const Pokemon = ({ name, skip }: { name: string; skip: boolean }) => {
370 * const { data, error, status } = useGetPokemonByNameQuery(name, {
371 * skip,
372 * });
373 *
374 * return (
375 * <div>
376 * {name} - {status}
377 * </div>
378 * );
379 * };
380 * ```
381 */
382 skip?: boolean
383 /**
384 * `selectFromResult` allows you to get a specific segment from a query result in a performant manner.
385 * When using this feature, the component will not rerender unless the underlying data of the selected item has changed.
386 * If the selected item is one element in a larger collection, it will disregard changes to elements in the same collection.
387 *
388 * @example
389 * ```ts
390 * // codeblock-meta title="Using selectFromResult to extract a single result"
391 * function PostsList() {
392 * const { data: posts } = api.useGetPostsQuery();
393 *
394 * return (
395 * <ul>
396 * {posts?.data?.map((post) => (
397 * <PostById key={post.id} id={post.id} />
398 * ))}
399 * </ul>
400 * );
401 * }
402 *
403 * function PostById({ id }: { id: number }) {
404 * // Will select the post with the given id, and will only rerender if the given posts data changes
405 * const { post } = api.useGetPostsQuery(undefined, {
406 * selectFromResult: ({ data }) => ({ post: data?.find((post) => post.id === id) }),
407 * });
408 *
409 * return <li>{post?.name}</li>;
410 * }
411 * ```
412 */
413 selectFromResult?: QueryStateSelector<R, D>
414}
415
416export type UseQueryStateResult<
417 _ extends QueryDefinition<any, any, any, any>,
418 R,
419> = TSHelpersNoInfer<R>
420
421/**
422 * Helper type to manually type the result
423 * of the `useQueryState` hook in userland code.
424 */
425export type TypedUseQueryStateResult<
426 ResultType,
427 QueryArg,
428 BaseQuery extends BaseQueryFn,
429 R = UseQueryStateDefaultResult<
430 QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>
431 >,
432> = TSHelpersNoInfer<R>
433
434type UseQueryStateBaseResult<D extends QueryDefinition<any, any, any, any>> =
435 QuerySubState<D> & {
436 /**
437 * Where `data` tries to hold data as much as possible, also re-using
438 * data from the last arguments passed into the hook, this property
439 * will always contain the received data from the query, for the current query arguments.
440 */
441 currentData?: ResultTypeFrom<D>
442 /**
443 * Query has not started yet.
444 */
445 isUninitialized: false
446 /**
447 * Query is currently loading for the first time. No data yet.
448 */
449 isLoading: false
450 /**
451 * Query is currently fetching, but might have data from an earlier request.
452 */
453 isFetching: false
454 /**
455 * Query has data from a successful load.
456 */
457 isSuccess: false
458 /**
459 * Query is currently in "error" state.
460 */
461 isError: false
462 }
463
464type UseQueryStateDefaultResult<D extends QueryDefinition<any, any, any, any>> =
465 TSHelpersId<
466 | TSHelpersOverride<
467 Extract<
468 UseQueryStateBaseResult<D>,
469 { status: QueryStatus.uninitialized }
470 >,
471 { isUninitialized: true }
472 >
473 | TSHelpersOverride<
474 UseQueryStateBaseResult<D>,
475 | { isLoading: true; isFetching: boolean; data: undefined }
476 | ({
477 isSuccess: true
478 isFetching: true
479 error: undefined
480 } & Required<
481 Pick<UseQueryStateBaseResult<D>, 'data' | 'fulfilledTimeStamp'>
482 >)
483 | ({
484 isSuccess: true
485 isFetching: false
486 error: undefined
487 } & Required<
488 Pick<
489 UseQueryStateBaseResult<D>,
490 'data' | 'fulfilledTimeStamp' | 'currentData'
491 >
492 >)
493 | ({ isError: true } & Required<
494 Pick<UseQueryStateBaseResult<D>, 'error'>
495 >)
496 >
497 > & {
498 /**
499 * @deprecated Included for completeness, but discouraged.
500 * Please use the `isLoading`, `isFetching`, `isSuccess`, `isError`
501 * and `isUninitialized` flags instead
502 */
503 status: QueryStatus
504 }
505
506export type MutationStateSelector<
507 R extends Record<string, any>,
508 D extends MutationDefinition<any, any, any, any>,
509> = (state: MutationResultSelectorResult<D>) => R
510
511export type UseMutationStateOptions<
512 D extends MutationDefinition<any, any, any, any>,
513 R extends Record<string, any>,
514> = {
515 selectFromResult?: MutationStateSelector<R, D>
516 fixedCacheKey?: string
517}
518
519export type UseMutationStateResult<
520 D extends MutationDefinition<any, any, any, any>,
521 R,
522> = TSHelpersNoInfer<R> & {
523 originalArgs?: QueryArgFrom<D>
524 /**
525 * Resets the hook state to it's initial `uninitialized` state.
526 * This will also remove the last result from the cache.
527 */
528 reset: () => void
529}
530
531/**
532 * Helper type to manually type the result
533 * of the `useMutation` hook in userland code.
534 */
535export type TypedUseMutationResult<
536 ResultType,
537 QueryArg,
538 BaseQuery extends BaseQueryFn,
539 R = MutationResultSelectorResult<
540 MutationDefinition<QueryArg, BaseQuery, string, ResultType, string>
541 >,
542> = UseMutationStateResult<
543 MutationDefinition<QueryArg, BaseQuery, string, ResultType, string>,
544 R
545>
546
547/**
548 * A React hook that lets you trigger an update request for a given endpoint, and subscribes the component to read the request status from the Redux store. The component will re-render as the loading status changes.
549 *
550 * #### Features
551 *
552 * - Manual control over firing a request to alter data on the server or possibly invalidate the cache
553 * - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
554 * - Returns the latest request status and cached data from the Redux store
555 * - Re-renders as the request status changes and data becomes available
556 */
557export type UseMutation<D extends MutationDefinition<any, any, any, any>> = <
558 R extends Record<string, any> = MutationResultSelectorResult<D>,
559>(
560 options?: UseMutationStateOptions<D, R>,
561) => readonly [MutationTrigger<D>, UseMutationStateResult<D, R>]
562
563export type TypedUseMutation<
564 ResultType,
565 QueryArg,
566 BaseQuery extends BaseQueryFn,
567> = UseMutation<
568 MutationDefinition<QueryArg, BaseQuery, string, ResultType, string>
569>
570
571export type MutationTrigger<D extends MutationDefinition<any, any, any, any>> =
572 {
573 /**
574 * Triggers the mutation and returns a Promise.
575 * @remarks
576 * If you need to access the error or success payload immediately after a mutation, you can chain .unwrap().
577 *
578 * @example
579 * ```ts
580 * // codeblock-meta title="Using .unwrap with async await"
581 * try {
582 * const payload = await addPost({ id: 1, name: 'Example' }).unwrap();
583 * console.log('fulfilled', payload)
584 * } catch (error) {
585 * console.error('rejected', error);
586 * }
587 * ```
588 */
589 (arg: QueryArgFrom<D>): MutationActionCreatorResult<D>
590 }
591
592export type TypedMutationTrigger<
593 ResultType,
594 QueryArg,
595 BaseQuery extends BaseQueryFn,
596> = MutationTrigger<
597 MutationDefinition<QueryArg, BaseQuery, string, ResultType, string>
598>
599
600/**
601 * Wrapper around `defaultQueryStateSelector` to be used in `useQuery`.
602 * We want the initial render to already come back with
603 * `{ isUninitialized: false, isFetching: true, isLoading: true }`
604 * to prevent that the library user has to do an additional check for `isUninitialized`/
605 */
606const noPendingQueryStateSelector: QueryStateSelector<any, any> = (
607 selected,
608) => {
609 if (selected.isUninitialized) {
610 return {
611 ...selected,
612 isUninitialized: false,
613 isFetching: true,
614 isLoading: selected.data !== undefined ? false : true,
615 status: QueryStatus.pending,
616 } as any
617 }
618 return selected
619}
620
621type GenericPrefetchThunk = (
622 endpointName: any,
623 arg: any,
624 options: PrefetchOptions,
625) => ThunkAction<void, any, any, UnknownAction>
626
627/**
628 *
629 * @param opts.api - An API with defined endpoints to create hooks for
630 * @param opts.moduleOptions.batch - The version of the `batchedUpdates` function to be used
631 * @param opts.moduleOptions.useDispatch - The version of the `useDispatch` hook to be used
632 * @param opts.moduleOptions.useSelector - The version of the `useSelector` hook to be used
633 * @returns An object containing functions to generate hooks based on an endpoint
634 */
635export function buildHooks<Definitions extends EndpointDefinitions>({
636 api,
637 moduleOptions: {
638 batch,
639 hooks: { useDispatch, useSelector, useStore },
640 unstable__sideEffectsInRender,
641 createSelector,
642 },
643 serializeQueryArgs,
644 context,
645}: {
646 api: Api<any, Definitions, any, any, CoreModule>
647 moduleOptions: Required<ReactHooksModuleOptions>
648 serializeQueryArgs: SerializeQueryArgs<any>
649 context: ApiContext<Definitions>
650}) {
651 const usePossiblyImmediateEffect: (
652 effect: () => void | undefined,
653 deps?: DependencyList,
654 ) => void = unstable__sideEffectsInRender ? (cb) => cb() : useEffect
655
656 return { buildQueryHooks, buildMutationHook, usePrefetch }
657
658 function queryStatePreSelector(
659 currentState: QueryResultSelectorResult<any>,
660 lastResult: UseQueryStateDefaultResult<any> | undefined,
661 queryArgs: any,
662 ): UseQueryStateDefaultResult<any> {
663 // if we had a last result and the current result is uninitialized,
664 // we might have called `api.util.resetApiState`
665 // in this case, reset the hook
666 if (lastResult?.endpointName && currentState.isUninitialized) {
667 const { endpointName } = lastResult
668 const endpointDefinition = context.endpointDefinitions[endpointName]
669 if (
670 serializeQueryArgs({
671 queryArgs: lastResult.originalArgs,
672 endpointDefinition,
673 endpointName,
674 }) ===
675 serializeQueryArgs({
676 queryArgs,
677 endpointDefinition,
678 endpointName,
679 })
680 )
681 lastResult = undefined
682 }
683
684 // data is the last known good request result we have tracked - or if none has been tracked yet the last good result for the current args
685 let data = currentState.isSuccess ? currentState.data : lastResult?.data
686 if (data === undefined) data = currentState.data
687
688 const hasData = data !== undefined
689
690 // isFetching = true any time a request is in flight
691 const isFetching = currentState.isLoading
692 // isLoading = true only when loading while no data is present yet (initial load with no data in the cache)
693 const isLoading = (!lastResult || lastResult.isLoading || lastResult.isUninitialized) && !hasData && isFetching
694 // isSuccess = true when data is present
695 const isSuccess = currentState.isSuccess || (isFetching && hasData)
696
697 return {
698 ...currentState,
699 data,
700 currentData: currentState.data,
701 isFetching,
702 isLoading,
703 isSuccess,
704 } as UseQueryStateDefaultResult<any>
705 }
706
707 function usePrefetch<EndpointName extends QueryKeys<Definitions>>(
708 endpointName: EndpointName,
709 defaultOptions?: PrefetchOptions,
710 ) {
711 const dispatch = useDispatch<ThunkDispatch<any, any, UnknownAction>>()
712 const stableDefaultOptions = useShallowStableValue(defaultOptions)
713
714 return useCallback(
715 (arg: any, options?: PrefetchOptions) =>
716 dispatch(
717 (api.util.prefetch as GenericPrefetchThunk)(endpointName, arg, {
718 ...stableDefaultOptions,
719 ...options,
720 }),
721 ),
722 [endpointName, dispatch, stableDefaultOptions],
723 )
724 }
725
726 function buildQueryHooks(name: string): QueryHooks<any> {
727 const useQuerySubscription: UseQuerySubscription<any> = (
728 arg: any,
729 {
730 refetchOnReconnect,
731 refetchOnFocus,
732 refetchOnMountOrArgChange,
733 skip = false,
734 pollingInterval = 0,
735 skipPollingIfUnfocused = false,
736 } = {},
737 ) => {
738 const { initiate } = api.endpoints[name] as ApiEndpointQuery<
739 QueryDefinition<any, any, any, any, any>,
740 Definitions
741 >
742 const dispatch = useDispatch<ThunkDispatch<any, any, UnknownAction>>()
743
744 // TODO: Change this to `useRef<SubscriptionSelectors>(undefined)` after upgrading to React 19.
745 /**
746 * @todo Change this to `useRef<SubscriptionSelectors>(undefined)` after upgrading to React 19.
747 */
748 const subscriptionSelectorsRef = useRef<
749 SubscriptionSelectors | undefined
750 >(undefined)
751
752 if (!subscriptionSelectorsRef.current) {
753 const returnedValue = dispatch(
754 api.internalActions.internal_getRTKQSubscriptions(),
755 )
756
757 if (process.env.NODE_ENV !== 'production') {
758 if (
759 typeof returnedValue !== 'object' ||
760 typeof returnedValue?.type === 'string'
761 ) {
762 throw new Error(
763 `Warning: Middleware for RTK-Query API at reducerPath "${api.reducerPath}" has not been added to the store.
764 You must add the middleware for RTK-Query to function correctly!`,
765 )
766 }
767 }
768
769 subscriptionSelectorsRef.current =
770 returnedValue as unknown as SubscriptionSelectors
771 }
772 const stableArg = useStableQueryArgs(
773 skip ? skipToken : arg,
774 // Even if the user provided a per-endpoint `serializeQueryArgs` with
775 // a consistent return value, _here_ we want to use the default behavior
776 // so we can tell if _anything_ actually changed. Otherwise, we can end up
777 // with a case where the query args did change but the serialization doesn't,
778 // and then we never try to initiate a refetch.
779 defaultSerializeQueryArgs,
780 context.endpointDefinitions[name],
781 name,
782 )
783 const stableSubscriptionOptions = useShallowStableValue({
784 refetchOnReconnect,
785 refetchOnFocus,
786 pollingInterval,
787 skipPollingIfUnfocused,
788 })
789
790 const lastRenderHadSubscription = useRef(false)
791
792 // TODO: Change this to `useRef<QueryActionCreatorResult<any>>(undefined)` after upgrading to React 19.
793 /**
794 * @todo Change this to `useRef<QueryActionCreatorResult<any>>(undefined)` after upgrading to React 19.
795 */
796 const promiseRef = useRef<QueryActionCreatorResult<any> | undefined>(
797 undefined,
798 )
799
800 let { queryCacheKey, requestId } = promiseRef.current || {}
801
802 // HACK We've saved the middleware subscription lookup callbacks into a ref,
803 // so we can directly check here if the subscription exists for this query.
804 let currentRenderHasSubscription = false
805 if (queryCacheKey && requestId) {
806 currentRenderHasSubscription =
807 subscriptionSelectorsRef.current.isRequestSubscribed(
808 queryCacheKey,
809 requestId,
810 )
811 }
812
813 const subscriptionRemoved =
814 !currentRenderHasSubscription && lastRenderHadSubscription.current
815
816 usePossiblyImmediateEffect(() => {
817 lastRenderHadSubscription.current = currentRenderHasSubscription
818 })
819
820 usePossiblyImmediateEffect((): void | undefined => {
821 if (subscriptionRemoved) {
822 promiseRef.current = undefined
823 }
824 }, [subscriptionRemoved])
825
826 usePossiblyImmediateEffect((): void | undefined => {
827 const lastPromise = promiseRef.current
828 if (
829 typeof process !== 'undefined' &&
830 process.env.NODE_ENV === 'removeMeOnCompilation'
831 ) {
832 // this is only present to enforce the rule of hooks to keep `isSubscribed` in the dependency array
833 console.log(subscriptionRemoved)
834 }
835
836 if (stableArg === skipToken) {
837 lastPromise?.unsubscribe()
838 promiseRef.current = undefined
839 return
840 }
841
842 const lastSubscriptionOptions = promiseRef.current?.subscriptionOptions
843
844 if (!lastPromise || lastPromise.arg !== stableArg) {
845 lastPromise?.unsubscribe()
846 const promise = dispatch(
847 initiate(stableArg, {
848 subscriptionOptions: stableSubscriptionOptions,
849 forceRefetch: refetchOnMountOrArgChange,
850 }),
851 )
852
853 promiseRef.current = promise
854 } else if (stableSubscriptionOptions !== lastSubscriptionOptions) {
855 lastPromise.updateSubscriptionOptions(stableSubscriptionOptions)
856 }
857 }, [
858 dispatch,
859 initiate,
860 refetchOnMountOrArgChange,
861 stableArg,
862 stableSubscriptionOptions,
863 subscriptionRemoved,
864 ])
865
866 useEffect(() => {
867 return () => {
868 promiseRef.current?.unsubscribe()
869 promiseRef.current = undefined
870 }
871 }, [])
872
873 return useMemo(
874 () => ({
875 /**
876 * A method to manually refetch data for the query
877 */
878 refetch: () => {
879 if (!promiseRef.current)
880 throw new Error(
881 'Cannot refetch a query that has not been started yet.',
882 )
883 return promiseRef.current?.refetch()
884 },
885 }),
886 [],
887 )
888 }
889
890 const useLazyQuerySubscription: UseLazyQuerySubscription<any> = ({
891 refetchOnReconnect,
892 refetchOnFocus,
893 pollingInterval = 0,
894 skipPollingIfUnfocused = false,
895 } = {}) => {
896 const { initiate } = api.endpoints[name] as ApiEndpointQuery<
897 QueryDefinition<any, any, any, any, any>,
898 Definitions
899 >
900 const dispatch = useDispatch<ThunkDispatch<any, any, UnknownAction>>()
901
902 const [arg, setArg] = useState<any>(UNINITIALIZED_VALUE)
903
904 // TODO: Change this to `useRef<QueryActionCreatorResult<any>>(undefined)` after upgrading to React 19.
905 /**
906 * @todo Change this to `useRef<QueryActionCreatorResult<any>>(undefined)` after upgrading to React 19.
907 */
908 const promiseRef = useRef<QueryActionCreatorResult<any> | undefined>(
909 undefined,
910 )
911
912 const stableSubscriptionOptions = useShallowStableValue({
913 refetchOnReconnect,
914 refetchOnFocus,
915 pollingInterval,
916 skipPollingIfUnfocused,
917 })
918
919 usePossiblyImmediateEffect(() => {
920 const lastSubscriptionOptions = promiseRef.current?.subscriptionOptions
921
922 if (stableSubscriptionOptions !== lastSubscriptionOptions) {
923 promiseRef.current?.updateSubscriptionOptions(
924 stableSubscriptionOptions,
925 )
926 }
927 }, [stableSubscriptionOptions])
928
929 const subscriptionOptionsRef = useRef(stableSubscriptionOptions)
930 usePossiblyImmediateEffect(() => {
931 subscriptionOptionsRef.current = stableSubscriptionOptions
932 }, [stableSubscriptionOptions])
933
934 const trigger = useCallback(
935 function (arg: any, preferCacheValue = false) {
936 let promise: QueryActionCreatorResult<any>
937
938 batch(() => {
939 promiseRef.current?.unsubscribe()
940
941 promiseRef.current = promise = dispatch(
942 initiate(arg, {
943 subscriptionOptions: subscriptionOptionsRef.current,
944 forceRefetch: !preferCacheValue,
945 }),
946 )
947
948 setArg(arg)
949 })
950
951 return promise!
952 },
953 [dispatch, initiate],
954 )
955
956 /* cleanup on unmount */
957 useEffect(() => {
958 return () => {
959 promiseRef?.current?.unsubscribe()
960 }
961 }, [])
962
963 /* if "cleanup on unmount" was triggered from a fast refresh, we want to reinstate the query */
964 useEffect(() => {
965 if (arg !== UNINITIALIZED_VALUE && !promiseRef.current) {
966 trigger(arg, true)
967 }
968 }, [arg, trigger])
969
970 return useMemo(() => [trigger, arg] as const, [trigger, arg])
971 }
972
973 const useQueryState: UseQueryState<any> = (
974 arg: any,
975 { skip = false, selectFromResult } = {},
976 ) => {
977 const { select } = api.endpoints[name] as ApiEndpointQuery<
978 QueryDefinition<any, any, any, any, any>,
979 Definitions
980 >
981 const stableArg = useStableQueryArgs(
982 skip ? skipToken : arg,
983 serializeQueryArgs,
984 context.endpointDefinitions[name],
985 name,
986 )
987
988 type ApiRootState = Parameters<ReturnType<typeof select>>[0]
989
990 const lastValue = useRef<any>(undefined)
991
992 const selectDefaultResult: Selector<ApiRootState, any, [any]> = useMemo(
993 () =>
994 createSelector(
995 [
996 select(stableArg),
997 (_: ApiRootState, lastResult: any) => lastResult,
998 (_: ApiRootState) => stableArg,
999 ],
1000 queryStatePreSelector,
1001 {
1002 memoizeOptions: {
1003 resultEqualityCheck: shallowEqual,
1004 },
1005 },
1006 ),
1007 [select, stableArg],
1008 )
1009
1010 const querySelector: Selector<ApiRootState, any, [any]> = useMemo(
1011 () =>
1012 selectFromResult
1013 ? createSelector([selectDefaultResult], selectFromResult, {
1014 devModeChecks: { identityFunctionCheck: 'never' },
1015 })
1016 : selectDefaultResult,
1017 [selectDefaultResult, selectFromResult],
1018 )
1019
1020 const currentState = useSelector(
1021 (state: RootState<Definitions, any, any>) =>
1022 querySelector(state, lastValue.current),
1023 shallowEqual,
1024 )
1025
1026 const store = useStore<RootState<Definitions, any, any>>()
1027 const newLastValue = selectDefaultResult(
1028 store.getState(),
1029 lastValue.current,
1030 )
1031 useIsomorphicLayoutEffect(() => {
1032 lastValue.current = newLastValue
1033 }, [newLastValue])
1034
1035 return currentState
1036 }
1037
1038 return {
1039 useQueryState,
1040 useQuerySubscription,
1041 useLazyQuerySubscription,
1042 useLazyQuery(options) {
1043 const [trigger, arg] = useLazyQuerySubscription(options)
1044 const queryStateResults = useQueryState(arg, {
1045 ...options,
1046 skip: arg === UNINITIALIZED_VALUE,
1047 })
1048
1049 const info = useMemo(() => ({ lastArg: arg }), [arg])
1050 return useMemo(
1051 () => [trigger, queryStateResults, info],
1052 [trigger, queryStateResults, info],
1053 )
1054 },
1055 useQuery(arg, options) {
1056 const querySubscriptionResults = useQuerySubscription(arg, options)
1057 const queryStateResults = useQueryState(arg, {
1058 selectFromResult:
1059 arg === skipToken || options?.skip
1060 ? undefined
1061 : noPendingQueryStateSelector,
1062 ...options,
1063 })
1064
1065 const { data, status, isLoading, isSuccess, isError, error } =
1066 queryStateResults
1067 useDebugValue({ data, status, isLoading, isSuccess, isError, error })
1068
1069 return useMemo(
1070 () => ({ ...queryStateResults, ...querySubscriptionResults }),
1071 [queryStateResults, querySubscriptionResults],
1072 )
1073 },
1074 }
1075 }
1076
1077 function buildMutationHook(name: string): UseMutation<any> {
1078 return ({ selectFromResult, fixedCacheKey } = {}) => {
1079 const { select, initiate } = api.endpoints[name] as ApiEndpointMutation<
1080 MutationDefinition<any, any, any, any, any>,
1081 Definitions
1082 >
1083 const dispatch = useDispatch<ThunkDispatch<any, any, UnknownAction>>()
1084 const [promise, setPromise] = useState<MutationActionCreatorResult<any>>()
1085
1086 useEffect(
1087 () => () => {
1088 if (!promise?.arg.fixedCacheKey) {
1089 promise?.reset()
1090 }
1091 },
1092 [promise],
1093 )
1094
1095 const triggerMutation = useCallback(
1096 function (arg: Parameters<typeof initiate>['0']) {
1097 const promise = dispatch(initiate(arg, { fixedCacheKey }))
1098 setPromise(promise)
1099 return promise
1100 },
1101 [dispatch, initiate, fixedCacheKey],
1102 )
1103
1104 const { requestId } = promise || {}
1105 const selectDefaultResult = useMemo(
1106 () => select({ fixedCacheKey, requestId: promise?.requestId }),
1107 [fixedCacheKey, promise, select],
1108 )
1109 const mutationSelector = useMemo(
1110 (): Selector<RootState<Definitions, any, any>, any> =>
1111 selectFromResult
1112 ? createSelector([selectDefaultResult], selectFromResult)
1113 : selectDefaultResult,
1114 [selectFromResult, selectDefaultResult],
1115 )
1116
1117 const currentState = useSelector(mutationSelector, shallowEqual)
1118 const originalArgs =
1119 fixedCacheKey == null ? promise?.arg.originalArgs : undefined
1120 const reset = useCallback(() => {
1121 batch(() => {
1122 if (promise) {
1123 setPromise(undefined)
1124 }
1125 if (fixedCacheKey) {
1126 dispatch(
1127 api.internalActions.removeMutationResult({
1128 requestId,
1129 fixedCacheKey,
1130 }),
1131 )
1132 }
1133 })
1134 }, [dispatch, fixedCacheKey, promise, requestId])
1135
1136 const {
1137 endpointName,
1138 data,
1139 status,
1140 isLoading,
1141 isSuccess,
1142 isError,
1143 error,
1144 } = currentState
1145 useDebugValue({
1146 endpointName,
1147 data,
1148 status,
1149 isLoading,
1150 isSuccess,
1151 isError,
1152 error,
1153 })
1154
1155 const finalState = useMemo(
1156 () => ({ ...currentState, originalArgs, reset }),
1157 [currentState, originalArgs, reset],
1158 )
1159
1160 return useMemo(
1161 () => [triggerMutation, finalState] as const,
1162 [triggerMutation, finalState],
1163 )
1164 }
1165 }
1166}