source: node_modules/@reduxjs/toolkit/src/createReducer.ts@ a762898

Last change on this file since a762898 was a762898, checked in by istevanoska <ilinastevanoska@…>, 5 months ago

Added visualizations

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