UNPKG

23.4 kBPlain TextView Raw
1/**
2 * Note: this file should import all other files for type discovery and declaration merging
3 */
4import type {
5 PatchQueryDataThunk,
6 UpdateQueryDataThunk,
7 UpsertQueryDataThunk,
8} from './buildThunks'
9import { buildThunks } from './buildThunks'
10import type {
11 ActionCreatorWithPayload,
12 Middleware,
13 Reducer,
14 ThunkAction,
15 ThunkDispatch,
16 UnknownAction,
17} from '@reduxjs/toolkit'
18import type {
19 EndpointDefinitions,
20 QueryArgFrom,
21 QueryDefinition,
22 MutationDefinition,
23 AssertTagTypes,
24 TagDescription,
25} from '../endpointDefinitions'
26import { isQueryDefinition, isMutationDefinition } from '../endpointDefinitions'
27import type {
28 CombinedState,
29 QueryKeys,
30 MutationKeys,
31 RootState,
32} from './apiState'
33import type { Api, Module } from '../apiTypes'
34import { onFocus, onFocusLost, onOnline, onOffline } from './setupListeners'
35import { buildSlice } from './buildSlice'
36import { buildMiddleware } from './buildMiddleware'
37import { buildSelectors } from './buildSelectors'
38import type {
39 MutationActionCreatorResult,
40 QueryActionCreatorResult,
41} from './buildInitiate'
42import { buildInitiate } from './buildInitiate'
43import { assertCast, safeAssign } from '../tsHelpers'
44import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
45import type { SliceActions } from './buildSlice'
46import type { BaseQueryFn } from '../baseQueryTypes'
47
48import type { ReferenceCacheLifecycle } from './buildMiddleware/cacheLifecycle'
49import type { ReferenceQueryLifecycle } from './buildMiddleware/queryLifecycle'
50import type { ReferenceCacheCollection } from './buildMiddleware/cacheCollection'
51import { enablePatches } from 'immer'
52import { createSelector as _createSelector } from './rtkImports'
53
54/**
55 * `ifOlderThan` - (default: `false` | `number`) - _number is value in seconds_
56 * - If specified, it will only run the query if the difference between `new Date()` and the last `fulfilledTimeStamp` is greater than the given value
57 *
58 * @overloadSummary
59 * `force`
60 * - If `force: true`, it will ignore the `ifOlderThan` value if it is set and the query will be run even if it exists in the cache.
61 */
62export type PrefetchOptions =
63 | {
64 ifOlderThan?: false | number
65 }
66 | { force?: boolean }
67
68export const coreModuleName = /* @__PURE__ */ Symbol()
69export type CoreModule =
70 | typeof coreModuleName
71 | ReferenceCacheLifecycle
72 | ReferenceQueryLifecycle
73 | ReferenceCacheCollection
74
75export interface ThunkWithReturnValue<T>
76 extends ThunkAction<T, any, any, UnknownAction> {}
77
78declare module '../apiTypes' {
79 export interface ApiModules<
80 // eslint-disable-next-line @typescript-eslint/no-unused-vars
81 BaseQuery extends BaseQueryFn,
82 Definitions extends EndpointDefinitions,
83 ReducerPath extends string,
84 TagTypes extends string,
85 > {
86 [coreModuleName]: {
87 /**
88 * This api's reducer should be mounted at `store[api.reducerPath]`.
89 *
90 * @example
91 * ```ts
92 * configureStore({
93 * reducer: {
94 * [api.reducerPath]: api.reducer,
95 * },
96 * middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(api.middleware),
97 * })
98 * ```
99 */
100 reducerPath: ReducerPath
101 /**
102 * Internal actions not part of the public API. Note: These are subject to change at any given time.
103 */
104 internalActions: InternalActions
105 /**
106 * A standard redux reducer that enables core functionality. Make sure it's included in your store.
107 *
108 * @example
109 * ```ts
110 * configureStore({
111 * reducer: {
112 * [api.reducerPath]: api.reducer,
113 * },
114 * middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(api.middleware),
115 * })
116 * ```
117 */
118 reducer: Reducer<
119 CombinedState<Definitions, TagTypes, ReducerPath>,
120 UnknownAction
121 >
122 /**
123 * This is a standard redux middleware and is responsible for things like polling, garbage collection and a handful of other things. Make sure it's included in your store.
124 *
125 * @example
126 * ```ts
127 * configureStore({
128 * reducer: {
129 * [api.reducerPath]: api.reducer,
130 * },
131 * middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(api.middleware),
132 * })
133 * ```
134 */
135 middleware: Middleware<
136 {},
137 RootState<Definitions, string, ReducerPath>,
138 ThunkDispatch<any, any, UnknownAction>
139 >
140 /**
141 * A collection of utility thunks for various situations.
142 */
143 util: {
144 /**
145 * A thunk that (if dispatched) will return a specific running query, identified
146 * by `endpointName` and `args`.
147 * If that query is not running, dispatching the thunk will result in `undefined`.
148 *
149 * Can be used to await a specific query triggered in any way,
150 * including via hook calls or manually dispatching `initiate` actions.
151 *
152 * See https://redux-toolkit.js.org/rtk-query/usage/server-side-rendering for details.
153 */
154 getRunningQueryThunk<EndpointName extends QueryKeys<Definitions>>(
155 endpointName: EndpointName,
156 args: QueryArgFrom<Definitions[EndpointName]>,
157 ): ThunkWithReturnValue<
158 | QueryActionCreatorResult<
159 Definitions[EndpointName] & { type: 'query' }
160 >
161 | undefined
162 >
163
164 /**
165 * A thunk that (if dispatched) will return a specific running mutation, identified
166 * by `endpointName` and `fixedCacheKey` or `requestId`.
167 * If that mutation is not running, dispatching the thunk will result in `undefined`.
168 *
169 * Can be used to await a specific mutation triggered in any way,
170 * including via hook trigger functions or manually dispatching `initiate` actions.
171 *
172 * See https://redux-toolkit.js.org/rtk-query/usage/server-side-rendering for details.
173 */
174 getRunningMutationThunk<EndpointName extends MutationKeys<Definitions>>(
175 endpointName: EndpointName,
176 fixedCacheKeyOrRequestId: string,
177 ): ThunkWithReturnValue<
178 | MutationActionCreatorResult<
179 Definitions[EndpointName] & { type: 'mutation' }
180 >
181 | undefined
182 >
183
184 /**
185 * A thunk that (if dispatched) will return all running queries.
186 *
187 * Useful for SSR scenarios to await all running queries triggered in any way,
188 * including via hook calls or manually dispatching `initiate` actions.
189 *
190 * See https://redux-toolkit.js.org/rtk-query/usage/server-side-rendering for details.
191 */
192 getRunningQueriesThunk(): ThunkWithReturnValue<
193 Array<QueryActionCreatorResult<any>>
194 >
195
196 /**
197 * A thunk that (if dispatched) will return all running mutations.
198 *
199 * Useful for SSR scenarios to await all running mutations triggered in any way,
200 * including via hook calls or manually dispatching `initiate` actions.
201 *
202 * See https://redux-toolkit.js.org/rtk-query/usage/server-side-rendering for details.
203 */
204 getRunningMutationsThunk(): ThunkWithReturnValue<
205 Array<MutationActionCreatorResult<any>>
206 >
207
208 /**
209 * A Redux thunk that can be used to manually trigger pre-fetching of data.
210 *
211 * The thunk accepts three arguments: the name of the endpoint we are updating (such as `'getPost'`), the appropriate query arg values to construct the desired cache key, and a set of options used to determine if the data actually should be re-fetched based on cache staleness.
212 *
213 * React Hooks users will most likely never need to use this directly, as the `usePrefetch` hook will dispatch this thunk internally as needed when you call the prefetching function supplied by the hook.
214 *
215 * @example
216 *
217 * ```ts no-transpile
218 * dispatch(api.util.prefetch('getPosts', undefined, { force: true }))
219 * ```
220 */
221 prefetch<EndpointName extends QueryKeys<Definitions>>(
222 endpointName: EndpointName,
223 arg: QueryArgFrom<Definitions[EndpointName]>,
224 options: PrefetchOptions,
225 ): ThunkAction<void, any, any, UnknownAction>
226 /**
227 * A Redux thunk action creator that, when dispatched, creates and applies a set of JSON diff/patch objects to the current state. This immediately updates the Redux state with those changes.
228 *
229 * The thunk action creator accepts three arguments: the name of the endpoint we are updating (such as `'getPost'`), the appropriate query arg values to construct the desired cache key, and an `updateRecipe` callback function. The callback receives an Immer-wrapped `draft` of the current state, and may modify the draft to match the expected results after the mutation completes successfully.
230 *
231 * The thunk executes _synchronously_, and returns an object containing `{patches: Patch[], inversePatches: Patch[], undo: () => void}`. The `patches` and `inversePatches` are generated using Immer's [`produceWithPatches` method](https://immerjs.github.io/immer/patches).
232 *
233 * This is typically used as the first step in implementing optimistic updates. The generated `inversePatches` can be used to revert the updates by calling `dispatch(patchQueryData(endpointName, args, inversePatches))`. Alternatively, the `undo` method can be called directly to achieve the same effect.
234 *
235 * Note that the first two arguments (`endpointName` and `args`) are used to determine which existing cache entry to update. If no existing cache entry is found, the `updateRecipe` callback will not run.
236 *
237 * @example
238 *
239 * ```ts
240 * const patchCollection = dispatch(
241 * api.util.updateQueryData('getPosts', undefined, (draftPosts) => {
242 * draftPosts.push({ id: 1, name: 'Teddy' })
243 * })
244 * )
245 * ```
246 */
247 updateQueryData: UpdateQueryDataThunk<
248 Definitions,
249 RootState<Definitions, string, ReducerPath>
250 >
251
252 /**
253 * A Redux thunk action creator that, when dispatched, acts as an artificial API request to upsert a value into the cache.
254 *
255 * The thunk action creator accepts three arguments: the name of the endpoint we are updating (such as `'getPost'`), the appropriate query arg values to construct the desired cache key, and the data to upsert.
256 *
257 * If no cache entry for that cache key exists, a cache entry will be created and the data added. If a cache entry already exists, this will _overwrite_ the existing cache entry data.
258 *
259 * The thunk executes _asynchronously_, and returns a promise that resolves when the store has been updated.
260 *
261 * If dispatched while an actual request is in progress, both the upsert and request will be handled as soon as they resolve, resulting in a "last result wins" update behavior.
262 *
263 * @example
264 *
265 * ```ts
266 * await dispatch(
267 * api.util.upsertQueryData('getPost', {id: 1}, {id: 1, text: "Hello!"})
268 * )
269 * ```
270 */
271 upsertQueryData: UpsertQueryDataThunk<
272 Definitions,
273 RootState<Definitions, string, ReducerPath>
274 >
275 /**
276 * A Redux thunk that applies a JSON diff/patch array to the cached data for a given query result. This immediately updates the Redux state with those changes.
277 *
278 * The thunk accepts three arguments: the name of the endpoint we are updating (such as `'getPost'`), the appropriate query arg values to construct the desired cache key, and a JSON diff/patch array as produced by Immer's `produceWithPatches`.
279 *
280 * This is typically used as the second step in implementing optimistic updates. If a request fails, the optimistically-applied changes can be reverted by dispatching `patchQueryData` with the `inversePatches` that were generated by `updateQueryData` earlier.
281 *
282 * In cases where it is desired to simply revert the previous changes, it may be preferable to call the `undo` method returned from dispatching `updateQueryData` instead.
283 *
284 * @example
285 * ```ts
286 * const patchCollection = dispatch(
287 * api.util.updateQueryData('getPosts', undefined, (draftPosts) => {
288 * draftPosts.push({ id: 1, name: 'Teddy' })
289 * })
290 * )
291 *
292 * // later
293 * dispatch(
294 * api.util.patchQueryData('getPosts', undefined, patchCollection.inversePatches)
295 * )
296 *
297 * // or
298 * patchCollection.undo()
299 * ```
300 */
301 patchQueryData: PatchQueryDataThunk<
302 Definitions,
303 RootState<Definitions, string, ReducerPath>
304 >
305
306 /**
307 * A Redux action creator that can be dispatched to manually reset the api state completely. This will immediately remove all existing cache entries, and all queries will be considered 'uninitialized'.
308 *
309 * @example
310 *
311 * ```ts
312 * dispatch(api.util.resetApiState())
313 * ```
314 */
315 resetApiState: SliceActions['resetApiState']
316 /**
317 * A Redux action creator that can be used to manually invalidate cache tags for [automated re-fetching](../../usage/automated-refetching.mdx).
318 *
319 * The action creator accepts one argument: the cache tags to be invalidated. It returns an action with those tags as a payload, and the corresponding `invalidateTags` action type for the api.
320 *
321 * Dispatching the result of this action creator will [invalidate](../../usage/automated-refetching.mdx#invalidating-cache-data) the given tags, causing queries to automatically re-fetch if they are subscribed to cache data that [provides](../../usage/automated-refetching.mdx#providing-cache-data) the corresponding tags.
322 *
323 * The array of tags provided to the action creator should be in one of the following formats, where `TagType` is equal to a string provided to the [`tagTypes`](../createApi.mdx#tagtypes) property of the api:
324 *
325 * - `[TagType]`
326 * - `[{ type: TagType }]`
327 * - `[{ type: TagType, id: number | string }]`
328 *
329 * @example
330 *
331 * ```ts
332 * dispatch(api.util.invalidateTags(['Post']))
333 * dispatch(api.util.invalidateTags([{ type: 'Post', id: 1 }]))
334 * dispatch(
335 * api.util.invalidateTags([
336 * { type: 'Post', id: 1 },
337 * { type: 'Post', id: 'LIST' },
338 * ])
339 * )
340 * ```
341 */
342 invalidateTags: ActionCreatorWithPayload<
343 Array<TagDescription<TagTypes>>,
344 string
345 >
346
347 /**
348 * A function to select all `{ endpointName, originalArgs, queryCacheKey }` combinations that would be invalidated by a specific set of tags.
349 *
350 * Can be used for mutations that want to do optimistic updates instead of invalidating a set of tags, but don't know exactly what they need to update.
351 */
352 selectInvalidatedBy: (
353 state: RootState<Definitions, string, ReducerPath>,
354 tags: ReadonlyArray<TagDescription<TagTypes>>,
355 ) => Array<{
356 endpointName: string
357 originalArgs: any
358 queryCacheKey: string
359 }>
360
361 /**
362 * A function to select all arguments currently cached for a given endpoint.
363 *
364 * Can be used for mutations that want to do optimistic updates instead of invalidating a set of tags, but don't know exactly what they need to update.
365 */
366 selectCachedArgsForQuery: <QueryName extends QueryKeys<Definitions>>(
367 state: RootState<Definitions, string, ReducerPath>,
368 queryName: QueryName,
369 ) => Array<QueryArgFrom<Definitions[QueryName]>>
370 }
371 /**
372 * Endpoints based on the input endpoints provided to `createApi`, containing `select` and `action matchers`.
373 */
374 endpoints: {
375 [K in keyof Definitions]: Definitions[K] extends QueryDefinition<
376 any,
377 any,
378 any,
379 any,
380 any
381 >
382 ? ApiEndpointQuery<Definitions[K], Definitions>
383 : Definitions[K] extends MutationDefinition<any, any, any, any, any>
384 ? ApiEndpointMutation<Definitions[K], Definitions>
385 : never
386 }
387 }
388 }
389}
390
391export interface ApiEndpointQuery<
392 // eslint-disable-next-line @typescript-eslint/no-unused-vars
393 Definition extends QueryDefinition<any, any, any, any, any>,
394 // eslint-disable-next-line @typescript-eslint/no-unused-vars
395 Definitions extends EndpointDefinitions,
396> {
397 name: string
398 /**
399 * All of these are `undefined` at runtime, purely to be used in TypeScript declarations!
400 */
401 Types: NonNullable<Definition['Types']>
402}
403
404// eslint-disable-next-line @typescript-eslint/no-unused-vars
405export interface ApiEndpointMutation<
406 // eslint-disable-next-line @typescript-eslint/no-unused-vars
407 Definition extends MutationDefinition<any, any, any, any, any>,
408 // eslint-disable-next-line @typescript-eslint/no-unused-vars
409 Definitions extends EndpointDefinitions,
410> {
411 name: string
412 /**
413 * All of these are `undefined` at runtime, purely to be used in TypeScript declarations!
414 */
415 Types: NonNullable<Definition['Types']>
416}
417
418export type ListenerActions = {
419 /**
420 * Will cause the RTK Query middleware to trigger any refetchOnReconnect-related behavior
421 * @link https://rtk-query-docs.netlify.app/api/setupListeners
422 */
423 onOnline: typeof onOnline
424 onOffline: typeof onOffline
425 /**
426 * Will cause the RTK Query middleware to trigger any refetchOnFocus-related behavior
427 * @link https://rtk-query-docs.netlify.app/api/setupListeners
428 */
429 onFocus: typeof onFocus
430 onFocusLost: typeof onFocusLost
431}
432
433export type InternalActions = SliceActions & ListenerActions
434
435export interface CoreModuleOptions {
436 /**
437 * A selector creator (usually from `reselect`, or matching the same signature)
438 */
439 createSelector?: typeof _createSelector
440}
441
442/**
443 * Creates a module containing the basic redux logic for use with `buildCreateApi`.
444 *
445 * @example
446 * ```ts
447 * const createBaseApi = buildCreateApi(coreModule());
448 * ```
449 */
450export const coreModule = ({
451 createSelector = _createSelector,
452}: CoreModuleOptions = {}): Module<CoreModule> => ({
453 name: coreModuleName,
454 init(
455 api,
456 {
457 baseQuery,
458 tagTypes,
459 reducerPath,
460 serializeQueryArgs,
461 keepUnusedDataFor,
462 refetchOnMountOrArgChange,
463 refetchOnFocus,
464 refetchOnReconnect,
465 invalidationBehavior,
466 },
467 context,
468 ) {
469 enablePatches()
470
471 assertCast<InternalSerializeQueryArgs>(serializeQueryArgs)
472
473 const assertTagType: AssertTagTypes = (tag) => {
474 if (
475 typeof process !== 'undefined' &&
476 process.env.NODE_ENV === 'development'
477 ) {
478 if (!tagTypes.includes(tag.type as any)) {
479 console.error(
480 `Tag type '${tag.type}' was used, but not specified in \`tagTypes\`!`,
481 )
482 }
483 }
484 return tag
485 }
486
487 Object.assign(api, {
488 reducerPath,
489 endpoints: {},
490 internalActions: {
491 onOnline,
492 onOffline,
493 onFocus,
494 onFocusLost,
495 },
496 util: {},
497 })
498
499 const {
500 queryThunk,
501 mutationThunk,
502 patchQueryData,
503 updateQueryData,
504 upsertQueryData,
505 prefetch,
506 buildMatchThunkActions,
507 } = buildThunks({
508 baseQuery,
509 reducerPath,
510 context,
511 api,
512 serializeQueryArgs,
513 assertTagType,
514 })
515
516 const { reducer, actions: sliceActions } = buildSlice({
517 context,
518 queryThunk,
519 mutationThunk,
520 reducerPath,
521 assertTagType,
522 config: {
523 refetchOnFocus,
524 refetchOnReconnect,
525 refetchOnMountOrArgChange,
526 keepUnusedDataFor,
527 reducerPath,
528 invalidationBehavior,
529 },
530 })
531
532 safeAssign(api.util, {
533 patchQueryData,
534 updateQueryData,
535 upsertQueryData,
536 prefetch,
537 resetApiState: sliceActions.resetApiState,
538 })
539 safeAssign(api.internalActions, sliceActions)
540
541 const { middleware, actions: middlewareActions } = buildMiddleware({
542 reducerPath,
543 context,
544 queryThunk,
545 mutationThunk,
546 api,
547 assertTagType,
548 })
549 safeAssign(api.util, middlewareActions)
550
551 safeAssign(api, { reducer: reducer as any, middleware })
552
553 const {
554 buildQuerySelector,
555 buildMutationSelector,
556 selectInvalidatedBy,
557 selectCachedArgsForQuery,
558 } = buildSelectors({
559 serializeQueryArgs: serializeQueryArgs as any,
560 reducerPath,
561 createSelector,
562 })
563
564 safeAssign(api.util, { selectInvalidatedBy, selectCachedArgsForQuery })
565
566 const {
567 buildInitiateQuery,
568 buildInitiateMutation,
569 getRunningMutationThunk,
570 getRunningMutationsThunk,
571 getRunningQueriesThunk,
572 getRunningQueryThunk,
573 } = buildInitiate({
574 queryThunk,
575 mutationThunk,
576 api,
577 serializeQueryArgs: serializeQueryArgs as any,
578 context,
579 })
580
581 safeAssign(api.util, {
582 getRunningMutationThunk,
583 getRunningMutationsThunk,
584 getRunningQueryThunk,
585 getRunningQueriesThunk,
586 })
587
588 return {
589 name: coreModuleName,
590 injectEndpoint(endpointName, definition) {
591 const anyApi = api as any as Api<
592 any,
593 Record<string, any>,
594 string,
595 string,
596 CoreModule
597 >
598 anyApi.endpoints[endpointName] ??= {} as any
599 if (isQueryDefinition(definition)) {
600 safeAssign(
601 anyApi.endpoints[endpointName],
602 {
603 name: endpointName,
604 select: buildQuerySelector(endpointName, definition),
605 initiate: buildInitiateQuery(endpointName, definition),
606 },
607 buildMatchThunkActions(queryThunk, endpointName),
608 )
609 } else if (isMutationDefinition(definition)) {
610 safeAssign(
611 anyApi.endpoints[endpointName],
612 {
613 name: endpointName,
614 select: buildMutationSelector(),
615 initiate: buildInitiateMutation(endpointName),
616 },
617 buildMatchThunkActions(mutationThunk, endpointName),
618 )
619 }
620 },
621 }
622 },
623})