| 1 | import type { Draft } from 'immer'
|
|---|
| 2 | import {
|
|---|
| 3 | createNextState,
|
|---|
| 4 | isDraft,
|
|---|
| 5 | isDraftable,
|
|---|
| 6 | setUseStrictIteration,
|
|---|
| 7 | } from './immerImports'
|
|---|
| 8 | import type { Action, Reducer, UnknownAction } from 'redux'
|
|---|
| 9 | import type { ActionReducerMapBuilder } from './mapBuilders'
|
|---|
| 10 | import { executeReducerBuilderCallback } from './mapBuilders'
|
|---|
| 11 | import type { NoInfer, TypeGuard } from './tsHelpers'
|
|---|
| 12 | import { 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 | */
|
|---|
| 22 | export type Actions<T extends keyof any = string> = Record<T, Action>
|
|---|
| 23 |
|
|---|
| 24 | export type ActionMatcherDescription<S, A extends Action> = {
|
|---|
| 25 | matcher: TypeGuard<A>
|
|---|
| 26 | reducer: CaseReducer<S, NoInfer<A>>
|
|---|
| 27 | }
|
|---|
| 28 |
|
|---|
| 29 | export type ReadonlyActionMatcherDescriptionCollection<S> = ReadonlyArray<
|
|---|
| 30 | ActionMatcherDescription<S, any>
|
|---|
| 31 | >
|
|---|
| 32 |
|
|---|
| 33 | export 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 | */
|
|---|
| 53 | export 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 | */
|
|---|
| 67 | export type CaseReducers<S, AS extends Actions> = {
|
|---|
| 68 | [T in keyof AS]: AS[T] extends Action ? CaseReducer<S, AS[T]> : void
|
|---|
| 69 | }
|
|---|
| 70 |
|
|---|
| 71 | export type NotFunction<T> = T extends Function ? never : T
|
|---|
| 72 |
|
|---|
| 73 | function isStateFunction<S>(x: unknown): x is () => S {
|
|---|
| 74 | return typeof x === 'function'
|
|---|
| 75 | }
|
|---|
| 76 |
|
|---|
| 77 | export 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
|
|---|
| 104 | import {
|
|---|
| 105 | createAction,
|
|---|
| 106 | createReducer,
|
|---|
| 107 | UnknownAction,
|
|---|
| 108 | PayloadAction,
|
|---|
| 109 | } from "@reduxjs/toolkit";
|
|---|
| 110 |
|
|---|
| 111 | const increment = createAction<number>("increment");
|
|---|
| 112 | const decrement = createAction<number>("decrement");
|
|---|
| 113 |
|
|---|
| 114 | function isActionWithNumberPayload(
|
|---|
| 115 | action: UnknownAction
|
|---|
| 116 | ): action is PayloadAction<number> {
|
|---|
| 117 | return typeof action.payload === "number";
|
|---|
| 118 | }
|
|---|
| 119 |
|
|---|
| 120 | const 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 | */
|
|---|
| 145 | export 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 | }
|
|---|