UNPKG

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