1 | import type { Action } from 'redux'
|
2 | import type {
|
3 | CaseReducer,
|
4 | CaseReducers,
|
5 | ActionMatcherDescriptionCollection,
|
6 | } from './createReducer'
|
7 | import type { TypeGuard } from './tsHelpers'
|
8 |
|
9 | export 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 | */
|
19 | export 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/2/narrowing.html#using-type-predicates)
|
50 | * function
|
51 | * @param reducer - The actual case reducer function.
|
52 | *
|
53 | * @example
|
54 | ```ts
|
55 | import {
|
56 | createAction,
|
57 | createReducer,
|
58 | AsyncThunk,
|
59 | UnknownAction,
|
60 | } from "@reduxjs/toolkit";
|
61 |
|
62 | type GenericAsyncThunk = AsyncThunk<unknown, unknown, any>;
|
63 |
|
64 | type PendingAction = ReturnType<GenericAsyncThunk["pending"]>;
|
65 | type RejectedAction = ReturnType<GenericAsyncThunk["rejected"]>;
|
66 | type FulfilledAction = ReturnType<GenericAsyncThunk["fulfilled"]>;
|
67 |
|
68 | const initialState: Record<string, string> = {};
|
69 | const resetAction = createAction("reset-tracked-loading-state");
|
70 |
|
71 | function isPendingAction(action: UnknownAction): action is PendingAction {
|
72 | return typeof action.type === "string" && action.type.endsWith("/pending");
|
73 | }
|
74 |
|
75 | const 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>(
|
100 | matcher: TypeGuard<A> | ((action: any) => boolean),
|
101 | reducer: CaseReducer<State, A extends Action ? A : A & Action>,
|
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
|
111 | import { createReducer } from '@reduxjs/toolkit'
|
112 | const initialState = { otherActions: 0 }
|
113 | const 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, Action>): {}
|
124 | }
|
125 |
|
126 | export function executeReducerBuilderCallback<S>(
|
127 | builderCallback: (builder: ActionReducerMapBuilder<S>) => void,
|
128 | ): [
|
129 | CaseReducers<S, any>,
|
130 | ActionMatcherDescriptionCollection<S>,
|
131 | CaseReducer<S, Action> | undefined,
|
132 | ] {
|
133 | const actionsMap: CaseReducers<S, any> = {}
|
134 | const actionMatchers: ActionMatcherDescriptionCollection<S> = []
|
135 | let defaultCaseReducer: CaseReducer<S, Action> | 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) {
|
163 | throw new Error(
|
164 | '`builder.addCase` cannot be called with an empty action type',
|
165 | )
|
166 | }
|
167 | if (type in actionsMap) {
|
168 | throw new Error(
|
169 | '`builder.addCase` cannot be called with two reducers for the same action type ' +
|
170 | `'${type}'`,
|
171 | )
|
172 | }
|
173 | actionsMap[type] = reducer
|
174 | return builder
|
175 | },
|
176 | addMatcher<A>(
|
177 | matcher: TypeGuard<A>,
|
178 | reducer: CaseReducer<S, A extends Action ? A : A & Action>,
|
179 | ) {
|
180 | if (process.env.NODE_ENV !== 'production') {
|
181 | if (defaultCaseReducer) {
|
182 | throw new Error(
|
183 | '`builder.addMatcher` should only be called before calling `builder.addDefaultCase`',
|
184 | )
|
185 | }
|
186 | }
|
187 | actionMatchers.push({ matcher, reducer })
|
188 | return builder
|
189 | },
|
190 | addDefaultCase(reducer: CaseReducer<S, Action>) {
|
191 | if (process.env.NODE_ENV !== 'production') {
|
192 | if (defaultCaseReducer) {
|
193 | throw new Error('`builder.addDefaultCase` can only be called once')
|
194 | }
|
195 | }
|
196 | defaultCaseReducer = reducer
|
197 | return builder
|
198 | },
|
199 | }
|
200 | builderCallback(builder)
|
201 | return [actionsMap, actionMatchers, defaultCaseReducer]
|
202 | }
|
203 |
|
\ | No newline at end of file |