1 | import type { Draft } from 'immer'
|
2 | import { produce as createNextState, isDraft, isDraftable } from 'immer'
|
3 | import type { Action, Reducer, UnknownAction } from 'redux'
|
4 | import type { ActionReducerMapBuilder } from './mapBuilders'
|
5 | import { executeReducerBuilderCallback } from './mapBuilders'
|
6 | import type { NoInfer, TypeGuard } from './tsHelpers'
|
7 | import { 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 | */
|
17 | export type Actions<T extends keyof any = string> = Record<T, Action>
|
18 |
|
19 | export type ActionMatcherDescription<S, A extends Action> = {
|
20 | matcher: TypeGuard<A>
|
21 | reducer: CaseReducer<S, NoInfer<A>>
|
22 | }
|
23 |
|
24 | export type ReadonlyActionMatcherDescriptionCollection<S> = ReadonlyArray<
|
25 | ActionMatcherDescription<S, any>
|
26 | >
|
27 |
|
28 | export 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 | */
|
48 | export 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 | */
|
62 | export type CaseReducers<S, AS extends Actions> = {
|
63 | [T in keyof AS]: AS[T] extends Action ? CaseReducer<S, AS[T]> : void
|
64 | }
|
65 |
|
66 | export type NotFunction<T> = T extends Function ? never : T
|
67 |
|
68 | function isStateFunction<S>(x: unknown): x is () => S {
|
69 | return typeof x === 'function'
|
70 | }
|
71 |
|
72 | export 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
|
99 | import {
|
100 | createAction,
|
101 | createReducer,
|
102 | UnknownAction,
|
103 | PayloadAction,
|
104 | } from "@reduxjs/toolkit";
|
105 |
|
106 | const increment = createAction<number>("increment");
|
107 | const decrement = createAction<number>("decrement");
|
108 |
|
109 | function isActionWithNumberPayload(
|
110 | action: UnknownAction
|
111 | ): action is PayloadAction<number> {
|
112 | return typeof action.payload === "number";
|
113 | }
|
114 |
|
115 | const 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 | */
|
140 | export 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 | }
|