UNPKG

@reduxjs/toolkit

Version:

The official, opinionated, batteries-included toolset for efficient Redux development

413 lines (371 loc) 12.2 kB
import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs' import type { EndpointDefinition, EndpointDefinitions, InfiniteQueryArgFrom, InfiniteQueryDefinition, MutationDefinition, QueryArgFrom, QueryArgFromAnyQuery, QueryDefinition, ReducerPathFrom, TagDescription, TagTypesFrom, } from '../endpointDefinitions' import { expandTagDescription } from '../endpointDefinitions' import { flatten, isNotNullish } from '../utils' import type { InfiniteData, InfiniteQueryConfigOptions, InfiniteQuerySubState, MutationSubState, QueryCacheKey, QueryKeys, QueryState, QuerySubState, RequestStatusFlags, RootState as _RootState, } from './apiState' import { QueryStatus, getRequestStatusFlags } from './apiState' import { getMutationCacheKey } from './buildSlice' import type { createSelector as _createSelector } from './rtkImports' import { createNextState } from './rtkImports' import { type AllQueryKeys, getNextPageParam, getPreviousPageParam, } from './buildThunks' export type SkipToken = typeof skipToken /** * Can be passed into `useQuery`, `useQueryState` or `useQuerySubscription` * instead of the query argument to get the same effect as if setting * `skip: true` in the query options. * * Useful for scenarios where a query should be skipped when `arg` is `undefined` * and TypeScript complains about it because `arg` is not allowed to be passed * in as `undefined`, such as * * ```ts * // codeblock-meta title="will error if the query argument is not allowed to be undefined" no-transpile * useSomeQuery(arg, { skip: !!arg }) * ``` * * ```ts * // codeblock-meta title="using skipToken instead" no-transpile * useSomeQuery(arg ?? skipToken) * ``` * * If passed directly into a query or mutation selector, that selector will always * return an uninitialized state. */ export const skipToken = /* @__PURE__ */ Symbol.for('RTKQ/skipToken') export type BuildSelectorsApiEndpointQuery< Definition extends QueryDefinition<any, any, any, any, any>, Definitions extends EndpointDefinitions, > = { select: QueryResultSelectorFactory< Definition, _RootState< Definitions, TagTypesFrom<Definition>, ReducerPathFrom<Definition> > > } export type BuildSelectorsApiEndpointInfiniteQuery< Definition extends InfiniteQueryDefinition<any, any, any, any, any>, Definitions extends EndpointDefinitions, > = { select: InfiniteQueryResultSelectorFactory< Definition, _RootState< Definitions, TagTypesFrom<Definition>, ReducerPathFrom<Definition> > > } export type BuildSelectorsApiEndpointMutation< Definition extends MutationDefinition<any, any, any, any, any>, Definitions extends EndpointDefinitions, > = { select: MutationResultSelectorFactory< Definition, _RootState< Definitions, TagTypesFrom<Definition>, ReducerPathFrom<Definition> > > } type QueryResultSelectorFactory< Definition extends QueryDefinition<any, any, any, any>, RootState, > = ( queryArg: QueryArgFrom<Definition> | SkipToken, ) => (state: RootState) => QueryResultSelectorResult<Definition> export type QueryResultSelectorResult< Definition extends QueryDefinition<any, any, any, any>, > = QuerySubState<Definition> & RequestStatusFlags type InfiniteQueryResultSelectorFactory< Definition extends InfiniteQueryDefinition<any, any, any, any, any>, RootState, > = ( queryArg: InfiniteQueryArgFrom<Definition> | SkipToken, ) => (state: RootState) => InfiniteQueryResultSelectorResult<Definition> export type InfiniteQueryResultFlags = { hasNextPage: boolean hasPreviousPage: boolean isFetchingNextPage: boolean isFetchingPreviousPage: boolean isFetchNextPageError: boolean isFetchPreviousPageError: boolean } export type InfiniteQueryResultSelectorResult< Definition extends InfiniteQueryDefinition<any, any, any, any, any>, > = InfiniteQuerySubState<Definition> & RequestStatusFlags & InfiniteQueryResultFlags type MutationResultSelectorFactory< Definition extends MutationDefinition<any, any, any, any>, RootState, > = ( requestId: | string | { requestId: string | undefined; fixedCacheKey: string | undefined } | SkipToken, ) => (state: RootState) => MutationResultSelectorResult<Definition> export type MutationResultSelectorResult< Definition extends MutationDefinition<any, any, any, any>, > = MutationSubState<Definition> & RequestStatusFlags const initialSubState: QuerySubState<any> = { status: QueryStatus.uninitialized as const, } // abuse immer to freeze default states const defaultQuerySubState = /* @__PURE__ */ createNextState( initialSubState, () => {}, ) const defaultMutationSubState = /* @__PURE__ */ createNextState( initialSubState as MutationSubState<any>, () => {}, ) export type AllSelectors = ReturnType<typeof buildSelectors> export function buildSelectors< Definitions extends EndpointDefinitions, ReducerPath extends string, >({ serializeQueryArgs, reducerPath, createSelector, }: { serializeQueryArgs: InternalSerializeQueryArgs reducerPath: ReducerPath createSelector: typeof _createSelector }) { type RootState = _RootState<Definitions, string, string> const selectSkippedQuery = (state: RootState) => defaultQuerySubState const selectSkippedMutation = (state: RootState) => defaultMutationSubState return { buildQuerySelector, buildInfiniteQuerySelector, buildMutationSelector, selectInvalidatedBy, selectCachedArgsForQuery, selectApiState, selectQueries, selectMutations, selectQueryEntry, selectConfig, } function withRequestFlags<T extends { status: QueryStatus }>( substate: T, ): T & RequestStatusFlags { return { ...substate, ...getRequestStatusFlags(substate.status) } } function selectApiState(rootState: RootState) { const state = rootState[reducerPath] if (process.env.NODE_ENV !== 'production') { if (!state) { if ((selectApiState as any).triggered) return state ;(selectApiState as any).triggered = true console.error( `Error: No data found at \`state.${reducerPath}\`. Did you forget to add the reducer to the store?`, ) } } return state } function selectQueries(rootState: RootState) { return selectApiState(rootState)?.queries } function selectQueryEntry(rootState: RootState, cacheKey: QueryCacheKey) { return selectQueries(rootState)?.[cacheKey] } function selectMutations(rootState: RootState) { return selectApiState(rootState)?.mutations } function selectConfig(rootState: RootState) { return selectApiState(rootState)?.config } function buildAnyQuerySelector( endpointName: string, endpointDefinition: EndpointDefinition<any, any, any, any>, combiner: <T extends { status: QueryStatus }>( substate: T, ) => T & RequestStatusFlags, ) { return (queryArgs: any) => { // Avoid calling serializeQueryArgs if the arg is skipToken if (queryArgs === skipToken) { return createSelector(selectSkippedQuery, combiner) } const serializedArgs = serializeQueryArgs({ queryArgs, endpointDefinition, endpointName, }) const selectQuerySubstate = (state: RootState) => selectQueryEntry(state, serializedArgs) ?? defaultQuerySubState return createSelector(selectQuerySubstate, combiner) } } function buildQuerySelector( endpointName: string, endpointDefinition: QueryDefinition<any, any, any, any>, ) { return buildAnyQuerySelector( endpointName, endpointDefinition, withRequestFlags, ) as QueryResultSelectorFactory<any, RootState> } function buildInfiniteQuerySelector( endpointName: string, endpointDefinition: InfiniteQueryDefinition<any, any, any, any, any>, ) { const { infiniteQueryOptions } = endpointDefinition function withInfiniteQueryResultFlags<T extends { status: QueryStatus }>( substate: T, ): T & RequestStatusFlags & InfiniteQueryResultFlags { const stateWithRequestFlags = { ...(substate as InfiniteQuerySubState<any>), ...getRequestStatusFlags(substate.status), } const { isLoading, isError, direction } = stateWithRequestFlags const isForward = direction === 'forward' const isBackward = direction === 'backward' return { ...stateWithRequestFlags, hasNextPage: getHasNextPage( infiniteQueryOptions, stateWithRequestFlags.data, ), hasPreviousPage: getHasPreviousPage( infiniteQueryOptions, stateWithRequestFlags.data, ), isFetchingNextPage: isLoading && isForward, isFetchingPreviousPage: isLoading && isBackward, isFetchNextPageError: isError && isForward, isFetchPreviousPageError: isError && isBackward, } } return buildAnyQuerySelector( endpointName, endpointDefinition, withInfiniteQueryResultFlags, ) as unknown as InfiniteQueryResultSelectorFactory<any, RootState> } function buildMutationSelector() { return ((id) => { let mutationId: string | typeof skipToken if (typeof id === 'object') { mutationId = getMutationCacheKey(id) ?? skipToken } else { mutationId = id } const selectMutationSubstate = (state: RootState) => selectApiState(state)?.mutations?.[mutationId as string] ?? defaultMutationSubState const finalSelectMutationSubstate = mutationId === skipToken ? selectSkippedMutation : selectMutationSubstate return createSelector(finalSelectMutationSubstate, withRequestFlags) }) as MutationResultSelectorFactory<any, RootState> } function selectInvalidatedBy( state: RootState, tags: ReadonlyArray<TagDescription<string> | null | undefined>, ): Array<{ endpointName: string originalArgs: any queryCacheKey: QueryCacheKey }> { const apiState = state[reducerPath] const toInvalidate = new Set<QueryCacheKey>() for (const tag of tags.filter(isNotNullish).map(expandTagDescription)) { const provided = apiState.provided.tags[tag.type] if (!provided) { continue } let invalidateSubscriptions = (tag.id !== undefined ? // id given: invalidate all queries that provide this type & id provided[tag.id] : // no id: invalidate all queries that provide this type flatten(Object.values(provided))) ?? [] for (const invalidate of invalidateSubscriptions) { toInvalidate.add(invalidate) } } return flatten( Array.from(toInvalidate.values()).map((queryCacheKey) => { const querySubState = apiState.queries[queryCacheKey] return querySubState ? [ { queryCacheKey, endpointName: querySubState.endpointName!, originalArgs: querySubState.originalArgs, }, ] : [] }), ) } function selectCachedArgsForQuery< QueryName extends AllQueryKeys<Definitions>, >( state: RootState, queryName: QueryName, ): Array<QueryArgFromAnyQuery<Definitions[QueryName]>> { return Object.values(selectQueries(state) as QueryState<any>) .filter( ( entry, ): entry is Exclude< QuerySubState<Definitions[QueryName]>, { status: QueryStatus.uninitialized } > => entry?.endpointName === queryName && entry.status !== QueryStatus.uninitialized, ) .map((entry) => entry.originalArgs) } function getHasNextPage( options: InfiniteQueryConfigOptions<any, any>, data?: InfiniteData<unknown, unknown>, ): boolean { if (!data) return false return getNextPageParam(options, data) != null } function getHasPreviousPage( options: InfiniteQueryConfigOptions<any, any>, data?: InfiniteData<unknown, unknown>, ): boolean { if (!data || !options.getPreviousPageParam) return false return getPreviousPageParam(options, data) != null } }