UNPKG

11.7 kBPlain TextView Raw
1import type { Draft } from 'immer'
2import createNextState, { isDraft, isDraftable } from 'immer'
3import type { AnyAction, Action, Reducer } from 'redux'
4import type { ActionReducerMapBuilder } from './mapBuilders'
5import { executeReducerBuilderCallback } from './mapBuilders'
6import type { NoInfer } from './tsHelpers'
7
8/**
9 * Defines a mapping from action types to corresponding action object shapes.
10 *
11 * @deprecated This should not be used manually - it is only used for internal
12 * inference purposes and should not have any further value.
13 * It might be removed in the future.
14 * @public
15 */
16export type Actions<T extends keyof any = string> = Record<T, Action>
17
18/**
19 * @deprecated use `TypeGuard` instead
20 */
21export interface ActionMatcher<A extends AnyAction> {
22 (action: AnyAction): action is A
23}
24
25export type ActionMatcherDescription<S, A extends AnyAction> = {
26 matcher: ActionMatcher<A>
27 reducer: CaseReducer<S, NoInfer<A>>
28}
29
30export type ReadonlyActionMatcherDescriptionCollection<S> = ReadonlyArray<
31 ActionMatcherDescription<S, any>
32>
33
34export type ActionMatcherDescriptionCollection<S> = Array<
35 ActionMatcherDescription<S, any>
36>
37
38/**
39 * An *case reducer* is a reducer function for a specific action type. Case
40 * reducers can be composed to full reducers using `createReducer()`.
41 *
42 * Unlike a normal Redux reducer, a case reducer is never called with an
43 * `undefined` state to determine the initial state. Instead, the initial
44 * state is explicitly specified as an argument to `createReducer()`.
45 *
46 * In addition, a case reducer can choose to mutate the passed-in `state`
47 * value directly instead of returning a new state. This does not actually
48 * cause the store state to be mutated directly; instead, thanks to
49 * [immer](https://github.com/mweststrate/immer), the mutations are
50 * translated to copy operations that result in a new state.
51 *
52 * @public
53 */
54export type CaseReducer<S = any, A extends Action = AnyAction> = (
55 state: Draft<S>,
56 action: A
57) => S | void | Draft<S>
58
59/**
60 * A mapping from action types to case reducers for `createReducer()`.
61 *
62 * @deprecated This should not be used manually - it is only used
63 * for internal inference purposes and using it manually
64 * would lead to type erasure.
65 * It might be removed in the future.
66 * @public
67 */
68export type CaseReducers<S, AS extends Actions> = {
69 [T in keyof AS]: AS[T] extends Action ? CaseReducer<S, AS[T]> : void
70}
71
72export type NotFunction<T> = T extends Function ? never : T
73
74function isStateFunction<S>(x: unknown): x is () => S {
75 return typeof x === 'function'
76}
77
78export type ReducerWithInitialState<S extends NotFunction<any>> = Reducer<S> & {
79 getInitialState: () => S
80}
81
82/**
83 * A utility function that allows defining a reducer as a mapping from action
84 * type to *case reducer* functions that handle these action types. The
85 * reducer's initial state is passed as the first argument.
86 *
87 * @remarks
88 * The body of every case reducer is implicitly wrapped with a call to
89 * `produce()` from the [immer](https://github.com/mweststrate/immer) library.
90 * This means that rather than returning a new state object, you can also
91 * mutate the passed-in state object directly; these mutations will then be
92 * automatically and efficiently translated into copies, giving you both
93 * convenience and immutability.
94 *
95 * @overloadSummary
96 * This overload accepts a callback function that receives a `builder` object as its argument.
97 * That builder provides `addCase`, `addMatcher` and `addDefaultCase` functions that may be
98 * called to define what actions this reducer will handle.
99 *
100 * @param initialState - `State | (() => State)`: The initial state that should be used when the reducer is called the first time. This may also be a "lazy initializer" function, which should return an initial state value when called. This will be used whenever the reducer is called with `undefined` as its state value, and is primarily useful for cases like reading initial state from `localStorage`.
101 * @param builderCallback - `(builder: Builder) => void` A callback that receives a *builder* object to define
102 * case reducers via calls to `builder.addCase(actionCreatorOrType, reducer)`.
103 * @example
104```ts
105import {
106 createAction,
107 createReducer,
108 AnyAction,
109 PayloadAction,
110} from "@reduxjs/toolkit";
111
112const increment = createAction<number>("increment");
113const decrement = createAction<number>("decrement");
114
115function isActionWithNumberPayload(
116 action: AnyAction
117): action is PayloadAction<number> {
118 return typeof action.payload === "number";
119}
120
121const reducer = createReducer(
122 {
123 counter: 0,
124 sumOfNumberPayloads: 0,
125 unhandledActions: 0,
126 },
127 (builder) => {
128 builder
129 .addCase(increment, (state, action) => {
130 // action is inferred correctly here
131 state.counter += action.payload;
132 })
133 // You can chain calls, or have separate `builder.addCase()` lines each time
134 .addCase(decrement, (state, action) => {
135 state.counter -= action.payload;
136 })
137 // You can apply a "matcher function" to incoming actions
138 .addMatcher(isActionWithNumberPayload, (state, action) => {})
139 // and provide a default case if no other handlers matched
140 .addDefaultCase((state, action) => {});
141 }
142);
143```
144 * @public
145 */
146export function createReducer<S extends NotFunction<any>>(
147 initialState: S | (() => S),
148 builderCallback: (builder: ActionReducerMapBuilder<S>) => void
149): ReducerWithInitialState<S>
150
151/**
152 * A utility function that allows defining a reducer as a mapping from action
153 * type to *case reducer* functions that handle these action types. The
154 * reducer's initial state is passed as the first argument.
155 *
156 * The body of every case reducer is implicitly wrapped with a call to
157 * `produce()` from the [immer](https://github.com/mweststrate/immer) library.
158 * This means that rather than returning a new state object, you can also
159 * mutate the passed-in state object directly; these mutations will then be
160 * automatically and efficiently translated into copies, giving you both
161 * convenience and immutability.
162 *
163 * @overloadSummary
164 * This overload accepts an object where the keys are string action types, and the values
165 * are case reducer functions to handle those action types.
166 *
167 * @param initialState - `State | (() => State)`: The initial state that should be used when the reducer is called the first time. This may also be a "lazy initializer" function, which should return an initial state value when called. This will be used whenever the reducer is called with `undefined` as its state value, and is primarily useful for cases like reading initial state from `localStorage`.
168 * @param actionsMap - An object mapping from action types to _case reducers_, each of which handles one specific action type.
169 * @param actionMatchers - An array of matcher definitions in the form `{matcher, reducer}`.
170 * All matching reducers will be executed in order, independently if a case reducer matched or not.
171 * @param defaultCaseReducer - A "default case" reducer that is executed if no case reducer and no matcher
172 * reducer was executed for this action.
173 *
174 * @example
175```js
176const counterReducer = createReducer(0, {
177 increment: (state, action) => state + action.payload,
178 decrement: (state, action) => state - action.payload
179})
180
181// Alternately, use a "lazy initializer" to provide the initial state
182// (works with either form of createReducer)
183const initialState = () => 0
184const counterReducer = createReducer(initialState, {
185 increment: (state, action) => state + action.payload,
186 decrement: (state, action) => state - action.payload
187})
188```
189
190 * Action creators that were generated using [`createAction`](./createAction) may be used directly as the keys here, using computed property syntax:
191
192```js
193const increment = createAction('increment')
194const decrement = createAction('decrement')
195
196const counterReducer = createReducer(0, {
197 [increment]: (state, action) => state + action.payload,
198 [decrement.type]: (state, action) => state - action.payload
199})
200```
201 * @public
202 */
203export function createReducer<
204 S extends NotFunction<any>,
205 CR extends CaseReducers<S, any> = CaseReducers<S, any>
206>(
207 initialState: S | (() => S),
208 actionsMap: CR,
209 actionMatchers?: ActionMatcherDescriptionCollection<S>,
210 defaultCaseReducer?: CaseReducer<S>
211): ReducerWithInitialState<S>
212
213export function createReducer<S extends NotFunction<any>>(
214 initialState: S | (() => S),
215 mapOrBuilderCallback:
216 | CaseReducers<S, any>
217 | ((builder: ActionReducerMapBuilder<S>) => void),
218 actionMatchers: ReadonlyActionMatcherDescriptionCollection<S> = [],
219 defaultCaseReducer?: CaseReducer<S>
220): ReducerWithInitialState<S> {
221 let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
222 typeof mapOrBuilderCallback === 'function'
223 ? executeReducerBuilderCallback(mapOrBuilderCallback)
224 : [mapOrBuilderCallback, actionMatchers, defaultCaseReducer]
225
226 // Ensure the initial state gets frozen either way
227 let getInitialState: () => S
228 if (isStateFunction(initialState)) {
229 getInitialState = () => createNextState(initialState(), () => {})
230 } else {
231 const frozenInitialState = createNextState(initialState, () => {})
232 getInitialState = () => frozenInitialState
233 }
234
235 function reducer(state = getInitialState(), action: any): S {
236 let caseReducers = [
237 actionsMap[action.type],
238 ...finalActionMatchers
239 .filter(({ matcher }) => matcher(action))
240 .map(({ reducer }) => reducer),
241 ]
242 if (caseReducers.filter((cr) => !!cr).length === 0) {
243 caseReducers = [finalDefaultCaseReducer]
244 }
245
246 return caseReducers.reduce((previousState, caseReducer): S => {
247 if (caseReducer) {
248 if (isDraft(previousState)) {
249 // If it's already a draft, we must already be inside a `createNextState` call,
250 // likely because this is being wrapped in `createReducer`, `createSlice`, or nested
251 // inside an existing draft. It's safe to just pass the draft to the mutator.
252 const draft = previousState as Draft<S> // We can assume this is already a draft
253 const result = caseReducer(draft, action)
254
255 if (typeof result === 'undefined') {
256 return previousState
257 }
258
259 return result as S
260 } else if (!isDraftable(previousState)) {
261 // If state is not draftable (ex: a primitive, such as 0), we want to directly
262 // return the caseReducer func and not wrap it with produce.
263 const result = caseReducer(previousState as any, action)
264
265 if (typeof result === 'undefined') {
266 if (previousState === null) {
267 return previousState
268 }
269 throw Error(
270 'A case reducer on a non-draftable value must not return undefined'
271 )
272 }
273
274 return result as S
275 } else {
276 // @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
277 // than an Immutable<S>, and TypeScript cannot find out how to reconcile
278 // these two types.
279 return createNextState(previousState, (draft: Draft<S>) => {
280 return caseReducer(draft, action)
281 })
282 }
283 }
284
285 return previousState
286 }, state)
287 }
288
289 reducer.getInitialState = getInitialState
290
291 return reducer as ReducerWithInitialState<S>
292}