UNPKG

@reduxjs/toolkit

Version:

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

388 lines (371 loc) 11.9 kB
import type { AsyncThunk, PayloadAction } from '@reduxjs/toolkit' import { combineReducers, createAction, createSlice, isAnyOf, isFulfilled, isRejectedWithValue, // Workaround for API-Extractor AnyAction, CombinedState, Reducer, ActionCreatorWithPayload, ActionCreatorWithoutPayload, } from '@reduxjs/toolkit' import type { CombinedState as CombinedQueryState, QuerySubstateIdentifier, QuerySubState, MutationSubstateIdentifier, MutationSubState, MutationState, QueryState, InvalidationState, Subscribers, QueryCacheKey, SubscriptionState, ConfigState, } from './apiState' import { QueryStatus } from './apiState' import type { MutationThunk, MutationThunkArg, QueryThunk, QueryThunkArg, ThunkResult, } from './buildThunks' import { calculateProvidedByThunk } from './buildThunks' import type { AssertTagTypes, EndpointDefinitions, } from '../endpointDefinitions' import type { Patch } from 'immer' import { applyPatches } from 'immer' import { onFocus, onFocusLost, onOffline, onOnline } from './setupListeners' import { isDocumentVisible, isOnline, copyWithStructuralSharing, } from '../utils' import type { ApiContext } from '../apiTypes' function updateQuerySubstateIfExists( state: QueryState<any>, queryCacheKey: QueryCacheKey, update: (substate: QuerySubState<any>) => void ) { const substate = state[queryCacheKey] if (substate) { update(substate) } } function updateMutationSubstateIfExists( state: MutationState<any>, { requestId }: MutationSubstateIdentifier, update: (substate: MutationSubState<any>) => void ) { const substate = state[requestId] if (substate) { update(substate) } } const initialState = {} as any export function buildSlice({ reducerPath, queryThunk, mutationThunk, context: { endpointDefinitions: definitions, apiUid }, assertTagType, config, }: { reducerPath: string queryThunk: QueryThunk mutationThunk: MutationThunk context: ApiContext<EndpointDefinitions> assertTagType: AssertTagTypes config: Omit< ConfigState<string>, 'online' | 'focused' | 'middlewareRegistered' > }) { const resetApiState = createAction(`${reducerPath}/resetApiState`) const querySlice = createSlice({ name: `${reducerPath}/queries`, initialState: initialState as QueryState<any>, reducers: { removeQueryResult( draft, { payload: { queryCacheKey } }: PayloadAction<QuerySubstateIdentifier> ) { delete draft[queryCacheKey] }, queryResultPatched( draft, { payload: { queryCacheKey, patches }, }: PayloadAction< QuerySubstateIdentifier & { patches: readonly Patch[] } > ) { updateQuerySubstateIfExists(draft, queryCacheKey, (substate) => { substate.data = applyPatches(substate.data as any, patches.concat()) }) }, }, extraReducers(builder) { builder .addCase(queryThunk.pending, (draft, { meta, meta: { arg } }) => { if (arg.subscribe) { // only initialize substate if we want to subscribe to it draft[arg.queryCacheKey] ??= { status: QueryStatus.uninitialized, endpointName: arg.endpointName, } } updateQuerySubstateIfExists(draft, arg.queryCacheKey, (substate) => { substate.status = QueryStatus.pending substate.requestId = meta.requestId substate.originalArgs = arg.originalArgs substate.startedTimeStamp = meta.startedTimeStamp }) }) .addCase(queryThunk.fulfilled, (draft, { meta, payload }) => { updateQuerySubstateIfExists( draft, meta.arg.queryCacheKey, (substate) => { if (substate.requestId !== meta.requestId) return substate.status = QueryStatus.fulfilled substate.data = copyWithStructuralSharing(substate.data, payload) delete substate.error substate.fulfilledTimeStamp = meta.fulfilledTimeStamp } ) }) .addCase( queryThunk.rejected, (draft, { meta: { condition, arg, requestId }, error, payload }) => { updateQuerySubstateIfExists( draft, arg.queryCacheKey, (substate) => { if (condition) { // request was aborted due to condition (another query already running) } else { // request failed if (substate.requestId !== requestId) return substate.status = QueryStatus.rejected substate.error = (payload ?? error) as any } } ) } ) }, }) const mutationSlice = createSlice({ name: `${reducerPath}/mutations`, initialState: initialState as MutationState<any>, reducers: { unsubscribeMutationResult( draft, action: PayloadAction<MutationSubstateIdentifier> ) { if (action.payload.requestId in draft) { delete draft[action.payload.requestId] } }, }, extraReducers(builder) { builder .addCase( mutationThunk.pending, (draft, { meta: { arg, requestId, startedTimeStamp } }) => { if (!arg.track) return draft[requestId] = { status: QueryStatus.pending, endpointName: arg.endpointName, startedTimeStamp, } } ) .addCase( mutationThunk.fulfilled, (draft, { payload, meta, meta: { requestId } }) => { if (!meta.arg.track) return updateMutationSubstateIfExists(draft, { requestId }, (substate) => { substate.status = QueryStatus.fulfilled substate.data = payload substate.fulfilledTimeStamp = meta.fulfilledTimeStamp }) } ) .addCase( mutationThunk.rejected, (draft, { payload, error, meta: { requestId, arg } }) => { if (!arg.track) return updateMutationSubstateIfExists(draft, { requestId }, (substate) => { substate.status = QueryStatus.rejected substate.error = (payload ?? error) as any }) } ) }, }) const invalidationSlice = createSlice({ name: `${reducerPath}/invalidation`, initialState: initialState as InvalidationState<string>, reducers: {}, extraReducers(builder) { builder .addCase( querySlice.actions.removeQueryResult, (draft, { payload: { queryCacheKey } }) => { for (const tagTypeSubscriptions of Object.values(draft)) { for (const idSubscriptions of Object.values( tagTypeSubscriptions )) { const foundAt = idSubscriptions.indexOf(queryCacheKey) if (foundAt !== -1) { idSubscriptions.splice(foundAt, 1) } } } } ) .addMatcher( isAnyOf(isFulfilled(queryThunk), isRejectedWithValue(queryThunk)), (draft, action) => { const providedTags = calculateProvidedByThunk( action, 'providesTags', definitions, assertTagType ) const { queryCacheKey } = action.meta.arg for (const { type, id } of providedTags) { const subscribedQueries = ((draft[type] ??= {})[ id || '__internal_without_id' ] ??= []) const alreadySubscribed = subscribedQueries.includes(queryCacheKey) if (!alreadySubscribed) { subscribedQueries.push(queryCacheKey) } } } ) }, }) const subscriptionSlice = createSlice({ name: `${reducerPath}/subscriptions`, initialState: initialState as SubscriptionState, reducers: { updateSubscriptionOptions( draft, { payload: { queryCacheKey, requestId, options }, }: PayloadAction< { endpointName: string requestId: string options: Subscribers[number] } & QuerySubstateIdentifier > ) { if (draft?.[queryCacheKey]?.[requestId]) { draft[queryCacheKey]![requestId] = options } }, unsubscribeQueryResult( draft, { payload: { queryCacheKey, requestId }, }: PayloadAction<{ requestId: string } & QuerySubstateIdentifier> ) { if (draft[queryCacheKey]) { delete draft[queryCacheKey]![requestId] } }, }, extraReducers: (builder) => { builder .addCase( querySlice.actions.removeQueryResult, (draft, { payload: { queryCacheKey } }) => { delete draft[queryCacheKey] } ) .addCase(queryThunk.pending, (draft, { meta: { arg, requestId } }) => { if (arg.subscribe) { const substate = (draft[arg.queryCacheKey] ??= {}) substate[requestId] = arg.subscriptionOptions ?? substate[requestId] ?? {} } }) .addCase( queryThunk.rejected, (draft, { meta: { condition, arg, requestId }, error, payload }) => { const substate = draft[arg.queryCacheKey] // request was aborted due to condition (another query already running) if (condition && arg.subscribe && substate) { substate[requestId] = arg.subscriptionOptions ?? substate[requestId] ?? {} } } ) }, }) const configSlice = createSlice({ name: `${reducerPath}/config`, initialState: { online: isOnline(), focused: isDocumentVisible(), middlewareRegistered: false, ...config, } as ConfigState<string>, reducers: { middlewareRegistered(state, { payload }: PayloadAction<string>) { state.middlewareRegistered = state.middlewareRegistered === 'conflict' || apiUid !== payload ? 'conflict' : true }, }, extraReducers: (builder) => { builder .addCase(onOnline, (state) => { state.online = true }) .addCase(onOffline, (state) => { state.online = false }) .addCase(onFocus, (state) => { state.focused = true }) .addCase(onFocusLost, (state) => { state.focused = false }) }, }) const combinedReducer = combineReducers< CombinedQueryState<any, string, string> >({ queries: querySlice.reducer, mutations: mutationSlice.reducer, provided: invalidationSlice.reducer, subscriptions: subscriptionSlice.reducer, config: configSlice.reducer, }) const reducer: typeof combinedReducer = (state, action) => combinedReducer(resetApiState.match(action) ? undefined : state, action) const actions = { ...configSlice.actions, ...querySlice.actions, ...subscriptionSlice.actions, ...mutationSlice.actions, resetApiState, } return { reducer, actions } } export type SliceActions = ReturnType<typeof buildSlice>['actions']