234 lines
8.6 KiB
TypeScript
234 lines
8.6 KiB
TypeScript
import type { Draft } from 'immer'
|
|
import {
|
|
createNextState,
|
|
isDraft,
|
|
isDraftable,
|
|
setUseStrictIteration,
|
|
} from './immerImports'
|
|
import type { Action, Reducer, UnknownAction } from 'redux'
|
|
import type { ActionReducerMapBuilder } from './mapBuilders'
|
|
import { executeReducerBuilderCallback } from './mapBuilders'
|
|
import type { NoInfer, TypeGuard } from './tsHelpers'
|
|
import { freezeDraftable } from './utils'
|
|
|
|
// Immer 10.2 defaults to still using strict iteration (specifically
|
|
// `Reflect.ownKeys()` for symbols support). However, we assume that
|
|
// Redux users are not using symbols as state keys, so we'll override
|
|
// this to prefer `Object.keys()` instead, as it provides a ~10% speedup.
|
|
// If users do need symbol support, they can call `setUseStrictIteration(true)` themselves.
|
|
setUseStrictIteration(false)
|
|
|
|
/**
|
|
* Defines a mapping from action types to corresponding action object shapes.
|
|
*
|
|
* @deprecated This should not be used manually - it is only used for internal
|
|
* inference purposes and should not have any further value.
|
|
* It might be removed in the future.
|
|
* @public
|
|
*/
|
|
export type Actions<T extends keyof any = string> = Record<T, Action>
|
|
|
|
export type ActionMatcherDescription<S, A extends Action> = {
|
|
matcher: TypeGuard<A>
|
|
reducer: CaseReducer<S, NoInfer<A>>
|
|
}
|
|
|
|
export type ReadonlyActionMatcherDescriptionCollection<S> = ReadonlyArray<
|
|
ActionMatcherDescription<S, any>
|
|
>
|
|
|
|
export type ActionMatcherDescriptionCollection<S> = Array<
|
|
ActionMatcherDescription<S, any>
|
|
>
|
|
|
|
/**
|
|
* A *case reducer* is a reducer function for a specific action type. Case
|
|
* reducers can be composed to full reducers using `createReducer()`.
|
|
*
|
|
* Unlike a normal Redux reducer, a case reducer is never called with an
|
|
* `undefined` state to determine the initial state. Instead, the initial
|
|
* state is explicitly specified as an argument to `createReducer()`.
|
|
*
|
|
* In addition, a case reducer can choose to mutate the passed-in `state`
|
|
* value directly instead of returning a new state. This does not actually
|
|
* cause the store state to be mutated directly; instead, thanks to
|
|
* [immer](https://github.com/mweststrate/immer), the mutations are
|
|
* translated to copy operations that result in a new state.
|
|
*
|
|
* @public
|
|
*/
|
|
export type CaseReducer<S = any, A extends Action = UnknownAction> = (
|
|
state: Draft<S>,
|
|
action: A,
|
|
) => NoInfer<S> | void | Draft<NoInfer<S>>
|
|
|
|
/**
|
|
* A mapping from action types to case reducers for `createReducer()`.
|
|
*
|
|
* @deprecated This should not be used manually - it is only used
|
|
* for internal inference purposes and using it manually
|
|
* would lead to type erasure.
|
|
* It might be removed in the future.
|
|
* @public
|
|
*/
|
|
export type CaseReducers<S, AS extends Actions> = {
|
|
[T in keyof AS]: AS[T] extends Action ? CaseReducer<S, AS[T]> : void
|
|
}
|
|
|
|
export type NotFunction<T> = T extends Function ? never : T
|
|
|
|
function isStateFunction<S>(x: unknown): x is () => S {
|
|
return typeof x === 'function'
|
|
}
|
|
|
|
export type ReducerWithInitialState<S extends NotFunction<any>> = Reducer<S> & {
|
|
getInitialState: () => S
|
|
}
|
|
|
|
/**
|
|
* A utility function that allows defining a reducer as a mapping from action
|
|
* type to *case reducer* functions that handle these action types. The
|
|
* reducer's initial state is passed as the first argument.
|
|
*
|
|
* @remarks
|
|
* The body of every case reducer is implicitly wrapped with a call to
|
|
* `produce()` from the [immer](https://github.com/mweststrate/immer) library.
|
|
* This means that rather than returning a new state object, you can also
|
|
* mutate the passed-in state object directly; these mutations will then be
|
|
* automatically and efficiently translated into copies, giving you both
|
|
* convenience and immutability.
|
|
*
|
|
* @overloadSummary
|
|
* This function accepts a callback that receives a `builder` object as its argument.
|
|
* That builder provides `addCase`, `addMatcher` and `addDefaultCase` functions that may be
|
|
* called to define what actions this reducer will handle.
|
|
*
|
|
* @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`.
|
|
* @param builderCallback - `(builder: Builder) => void` A callback that receives a *builder* object to define
|
|
* case reducers via calls to `builder.addCase(actionCreatorOrType, reducer)`.
|
|
* @example
|
|
```ts
|
|
import {
|
|
createAction,
|
|
createReducer,
|
|
UnknownAction,
|
|
PayloadAction,
|
|
} from "@reduxjs/toolkit";
|
|
|
|
const increment = createAction<number>("increment");
|
|
const decrement = createAction<number>("decrement");
|
|
|
|
function isActionWithNumberPayload(
|
|
action: UnknownAction
|
|
): action is PayloadAction<number> {
|
|
return typeof action.payload === "number";
|
|
}
|
|
|
|
const reducer = createReducer(
|
|
{
|
|
counter: 0,
|
|
sumOfNumberPayloads: 0,
|
|
unhandledActions: 0,
|
|
},
|
|
(builder) => {
|
|
builder
|
|
.addCase(increment, (state, action) => {
|
|
// action is inferred correctly here
|
|
state.counter += action.payload;
|
|
})
|
|
// You can chain calls, or have separate `builder.addCase()` lines each time
|
|
.addCase(decrement, (state, action) => {
|
|
state.counter -= action.payload;
|
|
})
|
|
// You can apply a "matcher function" to incoming actions
|
|
.addMatcher(isActionWithNumberPayload, (state, action) => {})
|
|
// and provide a default case if no other handlers matched
|
|
.addDefaultCase((state, action) => {});
|
|
}
|
|
);
|
|
```
|
|
* @public
|
|
*/
|
|
export function createReducer<S extends NotFunction<any>>(
|
|
initialState: S | (() => S),
|
|
mapOrBuilderCallback: (builder: ActionReducerMapBuilder<S>) => void,
|
|
): ReducerWithInitialState<S> {
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
if (typeof mapOrBuilderCallback === 'object') {
|
|
throw new Error(
|
|
"The object notation for `createReducer` has been removed. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createReducer",
|
|
)
|
|
}
|
|
}
|
|
|
|
let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
|
|
executeReducerBuilderCallback(mapOrBuilderCallback)
|
|
|
|
// Ensure the initial state gets frozen either way (if draftable)
|
|
let getInitialState: () => S
|
|
if (isStateFunction(initialState)) {
|
|
getInitialState = () => freezeDraftable(initialState())
|
|
} else {
|
|
const frozenInitialState = freezeDraftable(initialState)
|
|
getInitialState = () => frozenInitialState
|
|
}
|
|
|
|
function reducer(state = getInitialState(), action: any): S {
|
|
let caseReducers = [
|
|
actionsMap[action.type],
|
|
...finalActionMatchers
|
|
.filter(({ matcher }) => matcher(action))
|
|
.map(({ reducer }) => reducer),
|
|
]
|
|
if (caseReducers.filter((cr) => !!cr).length === 0) {
|
|
caseReducers = [finalDefaultCaseReducer]
|
|
}
|
|
|
|
return caseReducers.reduce((previousState, caseReducer): S => {
|
|
if (caseReducer) {
|
|
if (isDraft(previousState)) {
|
|
// If it's already a draft, we must already be inside a `createNextState` call,
|
|
// likely because this is being wrapped in `createReducer`, `createSlice`, or nested
|
|
// inside an existing draft. It's safe to just pass the draft to the mutator.
|
|
const draft = previousState as Draft<S> // We can assume this is already a draft
|
|
const result = caseReducer(draft, action)
|
|
|
|
if (result === undefined) {
|
|
return previousState
|
|
}
|
|
|
|
return result as S
|
|
} else if (!isDraftable(previousState)) {
|
|
// If state is not draftable (ex: a primitive, such as 0), we want to directly
|
|
// return the caseReducer func and not wrap it with produce.
|
|
const result = caseReducer(previousState as any, action)
|
|
|
|
if (result === undefined) {
|
|
if (previousState === null) {
|
|
return previousState
|
|
}
|
|
throw Error(
|
|
'A case reducer on a non-draftable value must not return undefined',
|
|
)
|
|
}
|
|
|
|
return result as S
|
|
} else {
|
|
// @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
|
|
// than an Immutable<S>, and TypeScript cannot find out how to reconcile
|
|
// these two types.
|
|
return createNextState(previousState, (draft: Draft<S>) => {
|
|
return caseReducer(draft, action)
|
|
})
|
|
}
|
|
}
|
|
|
|
return previousState
|
|
}, state)
|
|
}
|
|
|
|
reducer.getInitialState = getInitialState
|
|
|
|
return reducer as ReducerWithInitialState<S>
|
|
}
|