UNPKG

7.05 kBPlain TextView Raw
1import type { Action, AnyAction } from 'redux'
2import type {
3 CaseReducer,
4 CaseReducers,
5 ActionMatcher,
6 ActionMatcherDescriptionCollection,
7} from './createReducer'
8
9export interface TypedActionCreator<Type extends string> {
10 (...args: any[]): Action<Type>
11 type: Type
12}
13
14/**
15 * A builder for an action <-> reducer map.
16 *
17 * @public
18 */
19export interface ActionReducerMapBuilder<State> {
20 /**
21 * Adds a case reducer to handle a single exact action type.
22 * @remarks
23 * All calls to `builder.addCase` must come before any calls to `builder.addMatcher` or `builder.addDefaultCase`.
24 * @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.
25 * @param reducer - The actual case reducer function.
26 */
27 addCase<ActionCreator extends TypedActionCreator<string>>(
28 actionCreator: ActionCreator,
29 reducer: CaseReducer<State, ReturnType<ActionCreator>>
30 ): ActionReducerMapBuilder<State>
31 /**
32 * Adds a case reducer to handle a single exact action type.
33 * @remarks
34 * All calls to `builder.addCase` must come before any calls to `builder.addMatcher` or `builder.addDefaultCase`.
35 * @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.
36 * @param reducer - The actual case reducer function.
37 */
38 addCase<Type extends string, A extends Action<Type>>(
39 type: Type,
40 reducer: CaseReducer<State, A>
41 ): ActionReducerMapBuilder<State>
42
43 /**
44 * Allows you to match your incoming actions against your own filter function instead of only the `action.type` property.
45 * @remarks
46 * If multiple matcher reducers match, all of them will be executed in the order
47 * they were defined in - even if a case reducer already matched.
48 * All calls to `builder.addMatcher` must come after any calls to `builder.addCase` and before any calls to `builder.addDefaultCase`.
49 * @param matcher - A matcher function. In TypeScript, this should be a [type predicate](https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates)
50 * function
51 * @param reducer - The actual case reducer function.
52 *
53 * @example
54```ts
55import {
56 createAction,
57 createReducer,
58 AsyncThunk,
59 AnyAction,
60} from "@reduxjs/toolkit";
61
62type GenericAsyncThunk = AsyncThunk<unknown, unknown, any>;
63
64type PendingAction = ReturnType<GenericAsyncThunk["pending"]>;
65type RejectedAction = ReturnType<GenericAsyncThunk["rejected"]>;
66type FulfilledAction = ReturnType<GenericAsyncThunk["fulfilled"]>;
67
68const initialState: Record<string, string> = {};
69const resetAction = createAction("reset-tracked-loading-state");
70
71function isPendingAction(action: AnyAction): action is PendingAction {
72 return action.type.endsWith("/pending");
73}
74
75const reducer = createReducer(initialState, (builder) => {
76 builder
77 .addCase(resetAction, () => initialState)
78 // matcher can be defined outside as a type predicate function
79 .addMatcher(isPendingAction, (state, action) => {
80 state[action.meta.requestId] = "pending";
81 })
82 .addMatcher(
83 // matcher can be defined inline as a type predicate function
84 (action): action is RejectedAction => action.type.endsWith("/rejected"),
85 (state, action) => {
86 state[action.meta.requestId] = "rejected";
87 }
88 )
89 // matcher can just return boolean and the matcher can receive a generic argument
90 .addMatcher<FulfilledAction>(
91 (action) => action.type.endsWith("/fulfilled"),
92 (state, action) => {
93 state[action.meta.requestId] = "fulfilled";
94 }
95 );
96});
97```
98 */
99 addMatcher<A extends AnyAction>(
100 matcher: ActionMatcher<A> | ((action: AnyAction) => boolean),
101 reducer: CaseReducer<State, A>
102 ): Omit<ActionReducerMapBuilder<State>, 'addCase'>
103
104 /**
105 * Adds a "default case" reducer that is executed if no case reducer and no matcher
106 * reducer was executed for this action.
107 * @param reducer - The fallback "default case" reducer function.
108 *
109 * @example
110```ts
111import { createReducer } from '@reduxjs/toolkit'
112const initialState = { otherActions: 0 }
113const reducer = createReducer(initialState, builder => {
114 builder
115 // .addCase(...)
116 // .addMatcher(...)
117 .addDefaultCase((state, action) => {
118 state.otherActions++
119 })
120})
121```
122 */
123 addDefaultCase(reducer: CaseReducer<State, AnyAction>): {}
124}
125
126export function executeReducerBuilderCallback<S>(
127 builderCallback: (builder: ActionReducerMapBuilder<S>) => void
128): [
129 CaseReducers<S, any>,
130 ActionMatcherDescriptionCollection<S>,
131 CaseReducer<S, AnyAction> | undefined
132] {
133 const actionsMap: CaseReducers<S, any> = {}
134 const actionMatchers: ActionMatcherDescriptionCollection<S> = []
135 let defaultCaseReducer: CaseReducer<S, AnyAction> | undefined
136 const builder = {
137 addCase(
138 typeOrActionCreator: string | TypedActionCreator<any>,
139 reducer: CaseReducer<S>
140 ) {
141 if (process.env.NODE_ENV !== 'production') {
142 /*
143 to keep the definition by the user in line with actual behavior,
144 we enforce `addCase` to always be called before calling `addMatcher`
145 as matching cases take precedence over matchers
146 */
147 if (actionMatchers.length > 0) {
148 throw new Error(
149 '`builder.addCase` should only be called before calling `builder.addMatcher`'
150 )
151 }
152 if (defaultCaseReducer) {
153 throw new Error(
154 '`builder.addCase` should only be called before calling `builder.addDefaultCase`'
155 )
156 }
157 }
158 const type =
159 typeof typeOrActionCreator === 'string'
160 ? typeOrActionCreator
161 : typeOrActionCreator.type
162 if (type in actionsMap) {
163 throw new Error(
164 'addCase cannot be called with two reducers for the same action type'
165 )
166 }
167 actionsMap[type] = reducer
168 return builder
169 },
170 addMatcher<A extends AnyAction>(
171 matcher: ActionMatcher<A>,
172 reducer: CaseReducer<S, A>
173 ) {
174 if (process.env.NODE_ENV !== 'production') {
175 if (defaultCaseReducer) {
176 throw new Error(
177 '`builder.addMatcher` should only be called before calling `builder.addDefaultCase`'
178 )
179 }
180 }
181 actionMatchers.push({ matcher, reducer })
182 return builder
183 },
184 addDefaultCase(reducer: CaseReducer<S, AnyAction>) {
185 if (process.env.NODE_ENV !== 'production') {
186 if (defaultCaseReducer) {
187 throw new Error('`builder.addDefaultCase` can only be called once')
188 }
189 }
190 defaultCaseReducer = reducer
191 return builder
192 },
193 }
194 builderCallback(builder)
195 return [actionsMap, actionMatchers, defaultCaseReducer]
196}
197
\No newline at end of file