@reduxjs/toolkit
Version:
The official, opinionated, batteries-included toolset for efficient Redux development
261 lines (236 loc) • 9.92 kB
text/typescript
import type { Draft } from 'immer'
import createNextState, { isDraft, isDraftable } from 'immer'
import type { AnyAction, Action, Reducer } from 'redux'
import type { ActionReducerMapBuilder } from './mapBuilders'
import { executeReducerBuilderCallback } from './mapBuilders'
import type { NoInfer } from './tsHelpers'
/**
* 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 interface ActionMatcher<A extends AnyAction> {
(action: AnyAction): action is A
}
export type ActionMatcherDescription<S, A extends AnyAction> = {
matcher: ActionMatcher<A>
reducer: CaseReducer<S, NoInfer<A>>
}
export type ReadonlyActionMatcherDescriptionCollection<S> = ReadonlyArray<
ActionMatcherDescription<S, any>
>
export type ActionMatcherDescriptionCollection<S> = Array<
ActionMatcherDescription<S, any>
>
/**
* An *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 = AnyAction> = (
state: Draft<S>,
action: A
) => S | void | Draft<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
}
/**
* 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 overload accepts a callback function 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 - The initial state that should be used when the reducer is called the first time.
* @param builderCallback - A callback that receives a *builder* object to define
* case reducers via calls to `builder.addCase(actionCreatorOrType, reducer)`.
* @example
```ts
import {
createAction,
createReducer,
AnyAction,
PayloadAction,
} from "@reduxjs/toolkit";
const increment = createAction<number>("increment");
const decrement = createAction<number>("decrement");
function isActionWithNumberPayload(
action: AnyAction
): action is PayloadAction<number> {
return typeof action.payload === "number";
}
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>(
initialState: S,
builderCallback: (builder: ActionReducerMapBuilder<S>) => void
): Reducer<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.
*
* 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 overload accepts an object where the keys are string action types, and the values
* are case reducer functions to handle those action types.
*
* @param initialState - The initial state that should be used when the reducer is called the first time.
* @param actionsMap - An object mapping from action types to _case reducers_, each of which handles one specific action type.
* @param actionMatchers - An array of matcher definitions in the form `{matcher, reducer}`.
* All matching reducers will be executed in order, independently if a case reducer matched or not.
* @param defaultCaseReducer - A "default case" reducer that is executed if no case reducer and no matcher
* reducer was executed for this action.
*
* @example
```js
const counterReducer = createReducer(0, {
increment: (state, action) => state + action.payload,
decrement: (state, action) => state - action.payload
})
```
* Action creators that were generated using [`createAction`](./createAction) may be used directly as the keys here, using computed property syntax:
```js
const increment = createAction('increment')
const decrement = createAction('decrement')
const counterReducer = createReducer(0, {
[increment]: (state, action) => state + action.payload,
[decrement.type]: (state, action) => state - action.payload
})
```
* @public
*/
export function createReducer<
S,
CR extends CaseReducers<S, any> = CaseReducers<S, any>
>(
initialState: S,
actionsMap: CR,
actionMatchers?: ActionMatcherDescriptionCollection<S>,
defaultCaseReducer?: CaseReducer<S>
): Reducer<S>
export function createReducer<S>(
initialState: S,
mapOrBuilderCallback:
| CaseReducers<S, any>
| ((builder: ActionReducerMapBuilder<S>) => void),
actionMatchers: ReadonlyActionMatcherDescriptionCollection<S> = [],
defaultCaseReducer?: CaseReducer<S>
): Reducer<S> {
let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
typeof mapOrBuilderCallback === 'function'
? executeReducerBuilderCallback(mapOrBuilderCallback)
: [mapOrBuilderCallback, actionMatchers, defaultCaseReducer]
const frozenInitialState = createNextState(initialState, () => {})
return function (state = frozenInitialState, action): 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 (typeof 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 (typeof 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)
}
}