UNPKG

8.55 kBPlain TextView Raw
1import type { Draft } from 'immer'
2import { produce as createNextState, isDraft, isDraftable } from 'immer'
3import type { Action, Reducer, UnknownAction } from 'redux'
4import type { ActionReducerMapBuilder } from './mapBuilders'
5import { executeReducerBuilderCallback } from './mapBuilders'
6import type { NoInfer, TypeGuard } from './tsHelpers'
7import { freezeDraftable } from './utils'
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 type ActionMatcherDescription<S, A extends Action> = {
20 matcher: TypeGuard<A>
21 reducer: CaseReducer<S, NoInfer<A>>
22}
23
24export type ReadonlyActionMatcherDescriptionCollection<S> = ReadonlyArray<
25 ActionMatcherDescription<S, any>
26>
27
28export type ActionMatcherDescriptionCollection<S> = Array<
29 ActionMatcherDescription<S, any>
30>
31
32/**
33 * A *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 = UnknownAction> = (
49 state: Draft<S>,
50 action: A,
51) => NoInfer<S> | void | Draft<NoInfer<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
66export type NotFunction<T> = T extends Function ? never : T
67
68function isStateFunction<S>(x: unknown): x is () => S {
69 return typeof x === 'function'
70}
71
72export type ReducerWithInitialState<S extends NotFunction<any>> = Reducer<S> & {
73 getInitialState: () => S
74}
75
76/**
77 * A utility function that allows defining a reducer as a mapping from action
78 * type to *case reducer* functions that handle these action types. The
79 * reducer's initial state is passed as the first argument.
80 *
81 * @remarks
82 * The body of every case reducer is implicitly wrapped with a call to
83 * `produce()` from the [immer](https://github.com/mweststrate/immer) library.
84 * This means that rather than returning a new state object, you can also
85 * mutate the passed-in state object directly; these mutations will then be
86 * automatically and efficiently translated into copies, giving you both
87 * convenience and immutability.
88 *
89 * @overloadSummary
90 * This function accepts a callback that receives a `builder` object as its argument.
91 * That builder provides `addCase`, `addMatcher` and `addDefaultCase` functions that may be
92 * called to define what actions this reducer will handle.
93 *
94 * @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`.
95 * @param builderCallback - `(builder: Builder) => void` A callback that receives a *builder* object to define
96 * case reducers via calls to `builder.addCase(actionCreatorOrType, reducer)`.
97 * @example
98```ts
99import {
100 createAction,
101 createReducer,
102 UnknownAction,
103 PayloadAction,
104} from "@reduxjs/toolkit";
105
106const increment = createAction<number>("increment");
107const decrement = createAction<number>("decrement");
108
109function isActionWithNumberPayload(
110 action: UnknownAction
111): action is PayloadAction<number> {
112 return typeof action.payload === "number";
113}
114
115const reducer = createReducer(
116 {
117 counter: 0,
118 sumOfNumberPayloads: 0,
119 unhandledActions: 0,
120 },
121 (builder) => {
122 builder
123 .addCase(increment, (state, action) => {
124 // action is inferred correctly here
125 state.counter += action.payload;
126 })
127 // You can chain calls, or have separate `builder.addCase()` lines each time
128 .addCase(decrement, (state, action) => {
129 state.counter -= action.payload;
130 })
131 // You can apply a "matcher function" to incoming actions
132 .addMatcher(isActionWithNumberPayload, (state, action) => {})
133 // and provide a default case if no other handlers matched
134 .addDefaultCase((state, action) => {});
135 }
136);
137```
138 * @public
139 */
140export function createReducer<S extends NotFunction<any>>(
141 initialState: S | (() => S),
142 mapOrBuilderCallback: (builder: ActionReducerMapBuilder<S>) => void,
143): ReducerWithInitialState<S> {
144 if (process.env.NODE_ENV !== 'production') {
145 if (typeof mapOrBuilderCallback === 'object') {
146 throw new Error(
147 "The object notation for `createReducer` has been removed. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createReducer",
148 )
149 }
150 }
151
152 let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
153 executeReducerBuilderCallback(mapOrBuilderCallback)
154
155 // Ensure the initial state gets frozen either way (if draftable)
156 let getInitialState: () => S
157 if (isStateFunction(initialState)) {
158 getInitialState = () => freezeDraftable(initialState())
159 } else {
160 const frozenInitialState = freezeDraftable(initialState)
161 getInitialState = () => frozenInitialState
162 }
163
164 function reducer(state = getInitialState(), action: any): S {
165 let caseReducers = [
166 actionsMap[action.type],
167 ...finalActionMatchers
168 .filter(({ matcher }) => matcher(action))
169 .map(({ reducer }) => reducer),
170 ]
171 if (caseReducers.filter((cr) => !!cr).length === 0) {
172 caseReducers = [finalDefaultCaseReducer]
173 }
174
175 return caseReducers.reduce((previousState, caseReducer): S => {
176 if (caseReducer) {
177 if (isDraft(previousState)) {
178 // If it's already a draft, we must already be inside a `createNextState` call,
179 // likely because this is being wrapped in `createReducer`, `createSlice`, or nested
180 // inside an existing draft. It's safe to just pass the draft to the mutator.
181 const draft = previousState as Draft<S> // We can assume this is already a draft
182 const result = caseReducer(draft, action)
183
184 if (result === undefined) {
185 return previousState
186 }
187
188 return result as S
189 } else if (!isDraftable(previousState)) {
190 // If state is not draftable (ex: a primitive, such as 0), we want to directly
191 // return the caseReducer func and not wrap it with produce.
192 const result = caseReducer(previousState as any, action)
193
194 if (result === undefined) {
195 if (previousState === null) {
196 return previousState
197 }
198 throw Error(
199 'A case reducer on a non-draftable value must not return undefined',
200 )
201 }
202
203 return result as S
204 } else {
205 // @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
206 // than an Immutable<S>, and TypeScript cannot find out how to reconcile
207 // these two types.
208 return createNextState(previousState, (draft: Draft<S>) => {
209 return caseReducer(draft, action)
210 })
211 }
212 }
213
214 return previousState
215 }, state)
216 }
217
218 reducer.getInitialState = getInitialState
219
220 return reducer as ReducerWithInitialState<S>
221}