UNPKG

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