@reduxjs/toolkit
Version:
The official, opinionated, batteries-included toolset for efficient Redux development
590 lines (539 loc) • 17.5 kB
text/typescript
import type {
AsyncThunkAction,
SafePromise,
SerializedError,
ThunkAction,
UnknownAction,
} from '/toolkit'
import type { Dispatch } from 'redux'
import { asSafePromise } from '../../tsHelpers'
import type { Api, ApiContext } from '../apiTypes'
import type { BaseQueryError, QueryReturnValue } from '../baseQueryTypes'
import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
import {
isQueryDefinition,
type EndpointDefinition,
type EndpointDefinitions,
type InfiniteQueryArgFrom,
type InfiniteQueryDefinition,
type MutationDefinition,
type PageParamFrom,
type QueryArgFrom,
type QueryDefinition,
type ResultTypeFrom,
} from '../endpointDefinitions'
import { countObjectKeys, getOrInsert, isNotNullish } from '../utils'
import type {
InfiniteData,
InfiniteQueryConfigOptions,
InfiniteQueryDirection,
SubscriptionOptions,
} from './apiState'
import type {
InfiniteQueryResultSelectorResult,
QueryResultSelectorResult,
} from './buildSelectors'
import type {
InfiniteQueryThunk,
InfiniteQueryThunkArg,
MutationThunk,
QueryThunk,
QueryThunkArg,
ThunkApiMetaConfig,
} from './buildThunks'
import type { ApiEndpointQuery } from './module'
export type BuildInitiateApiEndpointQuery<
Definition extends QueryDefinition<any, any, any, any, any>,
> = {
initiate: StartQueryActionCreator<Definition>
}
export type BuildInitiateApiEndpointInfiniteQuery<
Definition extends InfiniteQueryDefinition<any, any, any, any, any>,
> = {
initiate: StartInfiniteQueryActionCreator<Definition>
}
export type BuildInitiateApiEndpointMutation<
Definition extends MutationDefinition<any, any, any, any, any>,
> = {
initiate: StartMutationActionCreator<Definition>
}
export const forceQueryFnSymbol = Symbol('forceQueryFn')
export const isUpsertQuery = (arg: QueryThunkArg) =>
typeof arg[forceQueryFnSymbol] === 'function'
export type StartQueryActionCreatorOptions = {
subscribe?: boolean
forceRefetch?: boolean | number
subscriptionOptions?: SubscriptionOptions
[forceQueryFnSymbol]?: () => QueryReturnValue
}
export type StartInfiniteQueryActionCreatorOptions<
D extends InfiniteQueryDefinition<any, any, any, any, any>,
> = StartQueryActionCreatorOptions & {
direction?: InfiniteQueryDirection
param?: unknown
} & Partial<
Pick<
Partial<InfiniteQueryConfigOptions<ResultTypeFrom<D>, PageParamFrom<D>>>,
'initialPageParam'
>
>
type AnyQueryActionCreator<D extends EndpointDefinition<any, any, any, any>> = (
arg: any,
options?: StartQueryActionCreatorOptions,
) => ThunkAction<AnyActionCreatorResult, any, any, UnknownAction>
type StartQueryActionCreator<
D extends QueryDefinition<any, any, any, any, any>,
> = (
arg: QueryArgFrom<D>,
options?: StartQueryActionCreatorOptions,
) => ThunkAction<QueryActionCreatorResult<D>, any, any, UnknownAction>
export type StartInfiniteQueryActionCreator<
D extends InfiniteQueryDefinition<any, any, any, any, any>,
> = (
arg: InfiniteQueryArgFrom<D>,
options?: StartInfiniteQueryActionCreatorOptions<D>,
) => ThunkAction<InfiniteQueryActionCreatorResult<D>, any, any, UnknownAction>
type QueryActionCreatorFields = {
requestId: string
subscriptionOptions: SubscriptionOptions | undefined
abort(): void
unsubscribe(): void
updateSubscriptionOptions(options: SubscriptionOptions): void
queryCacheKey: string
}
type AnyActionCreatorResult = SafePromise<any> &
QueryActionCreatorFields & {
arg: any
unwrap(): Promise<any>
refetch(): AnyActionCreatorResult
}
export type QueryActionCreatorResult<
D extends QueryDefinition<any, any, any, any>,
> = SafePromise<QueryResultSelectorResult<D>> &
QueryActionCreatorFields & {
arg: QueryArgFrom<D>
unwrap(): Promise<ResultTypeFrom<D>>
refetch(): QueryActionCreatorResult<D>
}
export type InfiniteQueryActionCreatorResult<
D extends InfiniteQueryDefinition<any, any, any, any, any>,
> = SafePromise<InfiniteQueryResultSelectorResult<D>> &
QueryActionCreatorFields & {
arg: InfiniteQueryArgFrom<D>
unwrap(): Promise<InfiniteData<ResultTypeFrom<D>, PageParamFrom<D>>>
refetch(): InfiniteQueryActionCreatorResult<D>
}
type StartMutationActionCreator<
D extends MutationDefinition<any, any, any, any>,
> = (
arg: QueryArgFrom<D>,
options?: {
/**
* If this mutation should be tracked in the store.
* If you just want to manually trigger this mutation using `dispatch` and don't care about the
* result, state & potential errors being held in store, you can set this to false.
* (defaults to `true`)
*/
track?: boolean
fixedCacheKey?: string
},
) => ThunkAction<MutationActionCreatorResult<D>, any, any, UnknownAction>
export type MutationActionCreatorResult<
D extends MutationDefinition<any, any, any, any>,
> = SafePromise<
| {
data: ResultTypeFrom<D>
error?: undefined
}
| {
data?: undefined
error:
| Exclude<
BaseQueryError<
D extends MutationDefinition<any, infer BaseQuery, any, any>
? BaseQuery
: never
>,
undefined
>
| SerializedError
}
> & {
/** @internal */
arg: {
/**
* The name of the given endpoint for the mutation
*/
endpointName: string
/**
* The original arguments supplied to the mutation call
*/
originalArgs: QueryArgFrom<D>
/**
* Whether the mutation is being tracked in the store.
*/
track?: boolean
fixedCacheKey?: string
}
/**
* A unique string generated for the request sequence
*/
requestId: string
/**
* A method to cancel the mutation promise. Note that this is not intended to prevent the mutation
* that was fired off from reaching the server, but only to assist in handling the response.
*
* Calling `abort()` prior to the promise resolving will force it to reach the error state with
* the serialized error:
* `{ name: 'AbortError', message: 'Aborted' }`
*
* @example
* ```ts
* const [updateUser] = useUpdateUserMutation();
*
* useEffect(() => {
* const promise = updateUser(id);
* promise
* .unwrap()
* .catch((err) => {
* if (err.name === 'AbortError') return;
* // else handle the unexpected error
* })
*
* return () => {
* promise.abort();
* }
* }, [id, updateUser])
* ```
*/
abort(): void
/**
* Unwraps a mutation call to provide the raw response/error.
*
* @remarks
* If you need to access the error or success payload immediately after a mutation, you can chain .unwrap().
*
* @example
* ```ts
* // codeblock-meta title="Using .unwrap"
* addPost({ id: 1, name: 'Example' })
* .unwrap()
* .then((payload) => console.log('fulfilled', payload))
* .catch((error) => console.error('rejected', error));
* ```
*
* @example
* ```ts
* // codeblock-meta title="Using .unwrap with async await"
* try {
* const payload = await addPost({ id: 1, name: 'Example' }).unwrap();
* console.log('fulfilled', payload)
* } catch (error) {
* console.error('rejected', error);
* }
* ```
*/
unwrap(): Promise<ResultTypeFrom<D>>
/**
* A method to manually unsubscribe from the mutation call, meaning it will be removed from cache after the usual caching grace period.
The value returned by the hook will reset to `isUninitialized` afterwards.
*/
reset(): void
}
export function buildInitiate({
serializeQueryArgs,
queryThunk,
infiniteQueryThunk,
mutationThunk,
api,
context,
}: {
serializeQueryArgs: InternalSerializeQueryArgs
queryThunk: QueryThunk
infiniteQueryThunk: InfiniteQueryThunk<any>
mutationThunk: MutationThunk
api: Api<any, EndpointDefinitions, any, any>
context: ApiContext<EndpointDefinitions>
}) {
const runningQueries: Map<
Dispatch,
Record<
string,
| QueryActionCreatorResult<any>
| InfiniteQueryActionCreatorResult<any>
| undefined
>
> = new Map()
const runningMutations: Map<
Dispatch,
Record<string, MutationActionCreatorResult<any> | undefined>
> = new Map()
const {
unsubscribeQueryResult,
removeMutationResult,
updateSubscriptionOptions,
} = api.internalActions
return {
buildInitiateQuery,
buildInitiateInfiniteQuery,
buildInitiateMutation,
getRunningQueryThunk,
getRunningMutationThunk,
getRunningQueriesThunk,
getRunningMutationsThunk,
}
function getRunningQueryThunk(endpointName: string, queryArgs: any) {
return (dispatch: Dispatch) => {
const endpointDefinition = context.endpointDefinitions[endpointName]
const queryCacheKey = serializeQueryArgs({
queryArgs,
endpointDefinition,
endpointName,
})
return runningQueries.get(dispatch)?.[queryCacheKey] as
| QueryActionCreatorResult<never>
| InfiniteQueryActionCreatorResult<never>
| undefined
}
}
function getRunningMutationThunk(
/**
* this is only here to allow TS to infer the result type by input value
* we could use it to validate the result, but it's probably not necessary
*/
_endpointName: string,
fixedCacheKeyOrRequestId: string,
) {
return (dispatch: Dispatch) => {
return runningMutations.get(dispatch)?.[fixedCacheKeyOrRequestId] as
| MutationActionCreatorResult<never>
| undefined
}
}
function getRunningQueriesThunk() {
return (dispatch: Dispatch) =>
Object.values(runningQueries.get(dispatch) || {}).filter(isNotNullish)
}
function getRunningMutationsThunk() {
return (dispatch: Dispatch) =>
Object.values(runningMutations.get(dispatch) || {}).filter(isNotNullish)
}
function middlewareWarning(dispatch: Dispatch) {
if (process.env.NODE_ENV !== 'production') {
if ((middlewareWarning as any).triggered) return
const returnedValue = dispatch(
api.internalActions.internal_getRTKQSubscriptions(),
)
;(middlewareWarning as any).triggered = true
// The RTKQ middleware should return the internal state object,
// but it should _not_ be the action object.
if (
typeof returnedValue !== 'object' ||
typeof returnedValue?.type === 'string'
) {
// Otherwise, must not have been added
throw new Error(
`Warning: Middleware for RTK-Query API at reducerPath "${api.reducerPath}" has not been added to the store.
You must add the middleware for RTK-Query to function correctly!`,
)
}
}
}
function buildInitiateAnyQuery<T extends 'query' | 'infiniteQuery'>(
endpointName: string,
endpointDefinition:
| QueryDefinition<any, any, any, any>
| InfiniteQueryDefinition<any, any, any, any, any>,
) {
const queryAction: AnyQueryActionCreator<any> =
(
arg,
{
subscribe = true,
forceRefetch,
subscriptionOptions,
[forceQueryFnSymbol]: forceQueryFn,
...rest
} = {},
) =>
(dispatch, getState) => {
const queryCacheKey = serializeQueryArgs({
queryArgs: arg,
endpointDefinition,
endpointName,
})
let thunk: AsyncThunkAction<unknown, QueryThunkArg, ThunkApiMetaConfig>
const commonThunkArgs = {
...rest,
type: 'query' as const,
subscribe,
forceRefetch: forceRefetch,
subscriptionOptions,
endpointName,
originalArgs: arg,
queryCacheKey,
[forceQueryFnSymbol]: forceQueryFn,
}
if (isQueryDefinition(endpointDefinition)) {
thunk = queryThunk(commonThunkArgs)
} else {
const { direction, initialPageParam } = rest as Pick<
InfiniteQueryThunkArg<any>,
'direction' | 'initialPageParam'
>
thunk = infiniteQueryThunk({
...(commonThunkArgs as InfiniteQueryThunkArg<any>),
// Supply these even if undefined. This helps with a field existence
// check over in `buildSlice.ts`
direction,
initialPageParam,
})
}
const selector = (
api.endpoints[endpointName] as ApiEndpointQuery<any, any>
).select(arg)
const thunkResult = dispatch(thunk)
const stateAfter = selector(getState())
middlewareWarning(dispatch)
const { requestId, abort } = thunkResult
const skippedSynchronously = stateAfter.requestId !== requestId
const runningQuery = runningQueries.get(dispatch)?.[queryCacheKey]
const selectFromState = () => selector(getState())
const statePromise: AnyActionCreatorResult = Object.assign(
(forceQueryFn
? // a query has been forced (upsertQueryData)
// -> we want to resolve it once data has been written with the data that will be written
thunkResult.then(selectFromState)
: skippedSynchronously && !runningQuery
? // a query has been skipped due to a condition and we do not have any currently running query
// -> we want to resolve it immediately with the current data
Promise.resolve(stateAfter)
: // query just started or one is already in flight
// -> wait for the running query, then resolve with data from after that
Promise.all([runningQuery, thunkResult]).then(
selectFromState,
)) as SafePromise<any>,
{
arg,
requestId,
subscriptionOptions,
queryCacheKey,
abort,
async unwrap() {
const result = await statePromise
if (result.isError) {
throw result.error
}
return result.data
},
refetch: () =>
dispatch(
queryAction(arg, { subscribe: false, forceRefetch: true }),
),
unsubscribe() {
if (subscribe)
dispatch(
unsubscribeQueryResult({
queryCacheKey,
requestId,
}),
)
},
updateSubscriptionOptions(options: SubscriptionOptions) {
statePromise.subscriptionOptions = options
dispatch(
updateSubscriptionOptions({
endpointName,
requestId,
queryCacheKey,
options,
}),
)
},
},
)
if (!runningQuery && !skippedSynchronously && !forceQueryFn) {
const running = getOrInsert(runningQueries, dispatch, {})
running[queryCacheKey] = statePromise
statePromise.then(() => {
delete running[queryCacheKey]
if (!countObjectKeys(running)) {
runningQueries.delete(dispatch)
}
})
}
return statePromise
}
return queryAction
}
function buildInitiateQuery(
endpointName: string,
endpointDefinition: QueryDefinition<any, any, any, any>,
) {
const queryAction: StartQueryActionCreator<any> = buildInitiateAnyQuery(
endpointName,
endpointDefinition,
)
return queryAction
}
function buildInitiateInfiniteQuery(
endpointName: string,
endpointDefinition: InfiniteQueryDefinition<any, any, any, any, any>,
) {
const infiniteQueryAction: StartInfiniteQueryActionCreator<any> =
buildInitiateAnyQuery(endpointName, endpointDefinition)
return infiniteQueryAction
}
function buildInitiateMutation(
endpointName: string,
): StartMutationActionCreator<any> {
return (arg, { track = true, fixedCacheKey } = {}) =>
(dispatch, getState) => {
const thunk = mutationThunk({
type: 'mutation',
endpointName,
originalArgs: arg,
track,
fixedCacheKey,
})
const thunkResult = dispatch(thunk)
middlewareWarning(dispatch)
const { requestId, abort, unwrap } = thunkResult
const returnValuePromise = asSafePromise(
thunkResult.unwrap().then((data) => ({ data })),
(error) => ({ error }),
)
const reset = () => {
dispatch(removeMutationResult({ requestId, fixedCacheKey }))
}
const ret = Object.assign(returnValuePromise, {
arg: thunkResult.arg,
requestId,
abort,
unwrap,
reset,
})
const running = runningMutations.get(dispatch) || {}
runningMutations.set(dispatch, running)
running[requestId] = ret
ret.then(() => {
delete running[requestId]
if (!countObjectKeys(running)) {
runningMutations.delete(dispatch)
}
})
if (fixedCacheKey) {
running[fixedCacheKey] = ret
ret.then(() => {
if (running[fixedCacheKey] === ret) {
delete running[fixedCacheKey]
if (!countObjectKeys(running)) {
runningMutations.delete(dispatch)
}
}
})
}
return ret
}
}
}