import type { Action } from 'redux' import type { CaseReducer, CaseReducers, ActionMatcherDescriptionCollection, } from './createReducer' import type { TypeGuard } from './tsHelpers' export interface TypedActionCreator { (...args: any[]): Action type: Type } /** * A builder for an action <-> reducer map. * * @public */ export interface ActionReducerMapBuilder { /** * Adds a case reducer to handle a single exact action type. * @remarks * All calls to `builder.addCase` must come before any calls to `builder.addMatcher` or `builder.addDefaultCase`. * @param actionCreator - Either a plain action type string, or an action creator generated by [`createAction`](./createAction) that can be used to determine the action type. * @param reducer - The actual case reducer function. */ addCase>( actionCreator: ActionCreator, reducer: CaseReducer>, ): ActionReducerMapBuilder /** * Adds a case reducer to handle a single exact action type. * @remarks * All calls to `builder.addCase` must come before any calls to `builder.addMatcher` or `builder.addDefaultCase`. * @param actionCreator - Either a plain action type string, or an action creator generated by [`createAction`](./createAction) that can be used to determine the action type. * @param reducer - The actual case reducer function. */ addCase>( type: Type, reducer: CaseReducer, ): ActionReducerMapBuilder /** * Allows you to match your incoming actions against your own filter function instead of only the `action.type` property. * @remarks * If multiple matcher reducers match, all of them will be executed in the order * they were defined in - even if a case reducer already matched. * All calls to `builder.addMatcher` must come after any calls to `builder.addCase` and before any calls to `builder.addDefaultCase`. * @param matcher - A matcher function. In TypeScript, this should be a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) * function * @param reducer - The actual case reducer function. * * @example ```ts import { createAction, createReducer, AsyncThunk, UnknownAction, } from "@reduxjs/toolkit"; type GenericAsyncThunk = AsyncThunk; type PendingAction = ReturnType; type RejectedAction = ReturnType; type FulfilledAction = ReturnType; const initialState: Record = {}; const resetAction = createAction("reset-tracked-loading-state"); function isPendingAction(action: UnknownAction): action is PendingAction { return typeof action.type === "string" && action.type.endsWith("/pending"); } const reducer = createReducer(initialState, (builder) => { builder .addCase(resetAction, () => initialState) // matcher can be defined outside as a type predicate function .addMatcher(isPendingAction, (state, action) => { state[action.meta.requestId] = "pending"; }) .addMatcher( // matcher can be defined inline as a type predicate function (action): action is RejectedAction => action.type.endsWith("/rejected"), (state, action) => { state[action.meta.requestId] = "rejected"; } ) // matcher can just return boolean and the matcher can receive a generic argument .addMatcher( (action) => action.type.endsWith("/fulfilled"), (state, action) => { state[action.meta.requestId] = "fulfilled"; } ); }); ``` */ addMatcher( matcher: TypeGuard | ((action: any) => boolean), reducer: CaseReducer, ): Omit, 'addCase'> /** * Adds a "default case" reducer that is executed if no case reducer and no matcher * reducer was executed for this action. * @param reducer - The fallback "default case" reducer function. * * @example ```ts import { createReducer } from '@reduxjs/toolkit' const initialState = { otherActions: 0 } const reducer = createReducer(initialState, builder => { builder // .addCase(...) // .addMatcher(...) .addDefaultCase((state, action) => { state.otherActions++ }) }) ``` */ addDefaultCase(reducer: CaseReducer): {} } export function executeReducerBuilderCallback( builderCallback: (builder: ActionReducerMapBuilder) => void, ): [ CaseReducers, ActionMatcherDescriptionCollection, CaseReducer | undefined, ] { const actionsMap: CaseReducers = {} const actionMatchers: ActionMatcherDescriptionCollection = [] let defaultCaseReducer: CaseReducer | undefined const builder = { addCase( typeOrActionCreator: string | TypedActionCreator, reducer: CaseReducer, ) { if (process.env.NODE_ENV !== 'production') { /* to keep the definition by the user in line with actual behavior, we enforce `addCase` to always be called before calling `addMatcher` as matching cases take precedence over matchers */ if (actionMatchers.length > 0) { throw new Error( '`builder.addCase` should only be called before calling `builder.addMatcher`', ) } if (defaultCaseReducer) { throw new Error( '`builder.addCase` should only be called before calling `builder.addDefaultCase`', ) } } const type = typeof typeOrActionCreator === 'string' ? typeOrActionCreator : typeOrActionCreator.type if (!type) { throw new Error( '`builder.addCase` cannot be called with an empty action type', ) } if (type in actionsMap) { throw new Error( '`builder.addCase` cannot be called with two reducers for the same action type ' + `'${type}'`, ) } actionsMap[type] = reducer return builder }, addMatcher( matcher: TypeGuard, reducer: CaseReducer, ) { if (process.env.NODE_ENV !== 'production') { if (defaultCaseReducer) { throw new Error( '`builder.addMatcher` should only be called before calling `builder.addDefaultCase`', ) } } actionMatchers.push({ matcher, reducer }) return builder }, addDefaultCase(reducer: CaseReducer) { if (process.env.NODE_ENV !== 'production') { if (defaultCaseReducer) { throw new Error('`builder.addDefaultCase` can only be called once') } } defaultCaseReducer = reducer return builder }, } builderCallback(builder) return [actionsMap, actionMatchers, defaultCaseReducer] }