| 1 | import type { Action } from 'redux'
|
|---|
| 2 | import type {
|
|---|
| 3 | CaseReducer,
|
|---|
| 4 | CaseReducers,
|
|---|
| 5 | ActionMatcherDescriptionCollection,
|
|---|
| 6 | } from './createReducer'
|
|---|
| 7 | import type { TypeGuard } from './tsHelpers'
|
|---|
| 8 | import type { AsyncThunk, AsyncThunkConfig } from './createAsyncThunk'
|
|---|
| 9 |
|
|---|
| 10 | export type AsyncThunkReducers<
|
|---|
| 11 | State,
|
|---|
| 12 | ThunkArg extends any,
|
|---|
| 13 | Returned = unknown,
|
|---|
| 14 | ThunkApiConfig extends AsyncThunkConfig = {},
|
|---|
| 15 | > = {
|
|---|
| 16 | pending?: CaseReducer<
|
|---|
| 17 | State,
|
|---|
| 18 | ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['pending']>
|
|---|
| 19 | >
|
|---|
| 20 | rejected?: CaseReducer<
|
|---|
| 21 | State,
|
|---|
| 22 | ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['rejected']>
|
|---|
| 23 | >
|
|---|
| 24 | fulfilled?: CaseReducer<
|
|---|
| 25 | State,
|
|---|
| 26 | ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['fulfilled']>
|
|---|
| 27 | >
|
|---|
| 28 | settled?: CaseReducer<
|
|---|
| 29 | State,
|
|---|
| 30 | ReturnType<
|
|---|
| 31 | AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['rejected' | 'fulfilled']
|
|---|
| 32 | >
|
|---|
| 33 | >
|
|---|
| 34 | }
|
|---|
| 35 |
|
|---|
| 36 | export type TypedActionCreator<Type extends string> = {
|
|---|
| 37 | (...args: any[]): Action<Type>
|
|---|
| 38 | type: Type
|
|---|
| 39 | }
|
|---|
| 40 |
|
|---|
| 41 | /**
|
|---|
| 42 | * A builder for an action <-> reducer map.
|
|---|
| 43 | *
|
|---|
| 44 | * @public
|
|---|
| 45 | */
|
|---|
| 46 | export interface ActionReducerMapBuilder<State> {
|
|---|
| 47 | /**
|
|---|
| 48 | * Adds a case reducer to handle a single exact action type.
|
|---|
| 49 | * @remarks
|
|---|
| 50 | * All calls to `builder.addCase` must come before any calls to `builder.addMatcher` or `builder.addDefaultCase`.
|
|---|
| 51 | * @param actionCreator - Either a plain action type string, or an action creator generated by [`createAction`](./createAction) that can be used to determine the action type.
|
|---|
| 52 | * @param reducer - The actual case reducer function.
|
|---|
| 53 | */
|
|---|
| 54 | addCase<ActionCreator extends TypedActionCreator<string>>(
|
|---|
| 55 | actionCreator: ActionCreator,
|
|---|
| 56 | reducer: CaseReducer<State, ReturnType<ActionCreator>>,
|
|---|
| 57 | ): ActionReducerMapBuilder<State>
|
|---|
| 58 | /**
|
|---|
| 59 | * Adds a case reducer to handle a single exact action type.
|
|---|
| 60 | * @remarks
|
|---|
| 61 | * All calls to `builder.addCase` must come before any calls to `builder.addAsyncThunk`, `builder.addMatcher` or `builder.addDefaultCase`.
|
|---|
| 62 | * @param actionCreator - Either a plain action type string, or an action creator generated by [`createAction`](./createAction) that can be used to determine the action type.
|
|---|
| 63 | * @param reducer - The actual case reducer function.
|
|---|
| 64 | */
|
|---|
| 65 | addCase<Type extends string, A extends Action<Type>>(
|
|---|
| 66 | type: Type,
|
|---|
| 67 | reducer: CaseReducer<State, A>,
|
|---|
| 68 | ): ActionReducerMapBuilder<State>
|
|---|
| 69 |
|
|---|
| 70 | /**
|
|---|
| 71 | * Adds case reducers to handle actions based on a `AsyncThunk` action creator.
|
|---|
| 72 | * @remarks
|
|---|
| 73 | * All calls to `builder.addAsyncThunk` must come before after any calls to `builder.addCase` and before any calls to `builder.addMatcher` or `builder.addDefaultCase`.
|
|---|
| 74 | * @param asyncThunk - The async thunk action creator itself.
|
|---|
| 75 | * @param reducers - A mapping from each of the `AsyncThunk` action types to the case reducer that should handle those actions.
|
|---|
| 76 | * @example
|
|---|
| 77 | ```ts no-transpile
|
|---|
| 78 | import { createAsyncThunk, createReducer } from '@reduxjs/toolkit'
|
|---|
| 79 |
|
|---|
| 80 | const fetchUserById = createAsyncThunk('users/fetchUser', async (id) => {
|
|---|
| 81 | const response = await fetch(`https://reqres.in/api/users/${id}`)
|
|---|
| 82 | return (await response.json()).data
|
|---|
| 83 | })
|
|---|
| 84 |
|
|---|
| 85 | const reducer = createReducer(initialState, (builder) => {
|
|---|
| 86 | builder.addAsyncThunk(fetchUserById, {
|
|---|
| 87 | pending: (state, action) => {
|
|---|
| 88 | state.fetchUserById.loading = 'pending'
|
|---|
| 89 | },
|
|---|
| 90 | fulfilled: (state, action) => {
|
|---|
| 91 | state.fetchUserById.data = action.payload
|
|---|
| 92 | },
|
|---|
| 93 | rejected: (state, action) => {
|
|---|
| 94 | state.fetchUserById.error = action.error
|
|---|
| 95 | },
|
|---|
| 96 | settled: (state, action) => {
|
|---|
| 97 | state.fetchUserById.loading = action.meta.requestStatus
|
|---|
| 98 | },
|
|---|
| 99 | })
|
|---|
| 100 | })
|
|---|
| 101 | */
|
|---|
| 102 | addAsyncThunk<
|
|---|
| 103 | Returned,
|
|---|
| 104 | ThunkArg,
|
|---|
| 105 | ThunkApiConfig extends AsyncThunkConfig = {},
|
|---|
| 106 | >(
|
|---|
| 107 | asyncThunk: AsyncThunk<Returned, ThunkArg, ThunkApiConfig>,
|
|---|
| 108 | reducers: AsyncThunkReducers<State, ThunkArg, Returned, ThunkApiConfig>,
|
|---|
| 109 | ): Omit<ActionReducerMapBuilder<State>, 'addCase'>
|
|---|
| 110 |
|
|---|
| 111 | /**
|
|---|
| 112 | * Allows you to match your incoming actions against your own filter function instead of only the `action.type` property.
|
|---|
| 113 | * @remarks
|
|---|
| 114 | * If multiple matcher reducers match, all of them will be executed in the order
|
|---|
| 115 | * they were defined in - even if a case reducer already matched.
|
|---|
| 116 | * All calls to `builder.addMatcher` must come after any calls to `builder.addCase` and `builder.addAsyncThunk` and before any calls to `builder.addDefaultCase`.
|
|---|
| 117 | * @param matcher - A matcher function. In TypeScript, this should be a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates)
|
|---|
| 118 | * function
|
|---|
| 119 | * @param reducer - The actual case reducer function.
|
|---|
| 120 | *
|
|---|
| 121 | * @example
|
|---|
| 122 | ```ts
|
|---|
| 123 | import {
|
|---|
| 124 | createAction,
|
|---|
| 125 | createReducer,
|
|---|
| 126 | AsyncThunk,
|
|---|
| 127 | UnknownAction,
|
|---|
| 128 | } from "@reduxjs/toolkit";
|
|---|
| 129 |
|
|---|
| 130 | type GenericAsyncThunk = AsyncThunk<unknown, unknown, any>;
|
|---|
| 131 |
|
|---|
| 132 | type PendingAction = ReturnType<GenericAsyncThunk["pending"]>;
|
|---|
| 133 | type RejectedAction = ReturnType<GenericAsyncThunk["rejected"]>;
|
|---|
| 134 | type FulfilledAction = ReturnType<GenericAsyncThunk["fulfilled"]>;
|
|---|
| 135 |
|
|---|
| 136 | const initialState: Record<string, string> = {};
|
|---|
| 137 | const resetAction = createAction("reset-tracked-loading-state");
|
|---|
| 138 |
|
|---|
| 139 | function isPendingAction(action: UnknownAction): action is PendingAction {
|
|---|
| 140 | return typeof action.type === "string" && action.type.endsWith("/pending");
|
|---|
| 141 | }
|
|---|
| 142 |
|
|---|
| 143 | const reducer = createReducer(initialState, (builder) => {
|
|---|
| 144 | builder
|
|---|
| 145 | .addCase(resetAction, () => initialState)
|
|---|
| 146 | // matcher can be defined outside as a type predicate function
|
|---|
| 147 | .addMatcher(isPendingAction, (state, action) => {
|
|---|
| 148 | state[action.meta.requestId] = "pending";
|
|---|
| 149 | })
|
|---|
| 150 | .addMatcher(
|
|---|
| 151 | // matcher can be defined inline as a type predicate function
|
|---|
| 152 | (action): action is RejectedAction => action.type.endsWith("/rejected"),
|
|---|
| 153 | (state, action) => {
|
|---|
| 154 | state[action.meta.requestId] = "rejected";
|
|---|
| 155 | }
|
|---|
| 156 | )
|
|---|
| 157 | // matcher can just return boolean and the matcher can receive a generic argument
|
|---|
| 158 | .addMatcher<FulfilledAction>(
|
|---|
| 159 | (action) => action.type.endsWith("/fulfilled"),
|
|---|
| 160 | (state, action) => {
|
|---|
| 161 | state[action.meta.requestId] = "fulfilled";
|
|---|
| 162 | }
|
|---|
| 163 | );
|
|---|
| 164 | });
|
|---|
| 165 | ```
|
|---|
| 166 | */
|
|---|
| 167 | addMatcher<A>(
|
|---|
| 168 | matcher: TypeGuard<A> | ((action: any) => boolean),
|
|---|
| 169 | reducer: CaseReducer<State, A extends Action ? A : A & Action>,
|
|---|
| 170 | ): Omit<ActionReducerMapBuilder<State>, 'addCase' | 'addAsyncThunk'>
|
|---|
| 171 |
|
|---|
| 172 | /**
|
|---|
| 173 | * Adds a "default case" reducer that is executed if no case reducer and no matcher
|
|---|
| 174 | * reducer was executed for this action.
|
|---|
| 175 | * @param reducer - The fallback "default case" reducer function.
|
|---|
| 176 | *
|
|---|
| 177 | * @example
|
|---|
| 178 | ```ts
|
|---|
| 179 | import { createReducer } from '@reduxjs/toolkit'
|
|---|
| 180 | const initialState = { otherActions: 0 }
|
|---|
| 181 | const reducer = createReducer(initialState, builder => {
|
|---|
| 182 | builder
|
|---|
| 183 | // .addCase(...)
|
|---|
| 184 | // .addMatcher(...)
|
|---|
| 185 | .addDefaultCase((state, action) => {
|
|---|
| 186 | state.otherActions++
|
|---|
| 187 | })
|
|---|
| 188 | })
|
|---|
| 189 | ```
|
|---|
| 190 | */
|
|---|
| 191 | addDefaultCase(reducer: CaseReducer<State, Action>): {}
|
|---|
| 192 | }
|
|---|
| 193 |
|
|---|
| 194 | export function executeReducerBuilderCallback<S>(
|
|---|
| 195 | builderCallback: (builder: ActionReducerMapBuilder<S>) => void,
|
|---|
| 196 | ): [
|
|---|
| 197 | CaseReducers<S, any>,
|
|---|
| 198 | ActionMatcherDescriptionCollection<S>,
|
|---|
| 199 | CaseReducer<S, Action> | undefined,
|
|---|
| 200 | ] {
|
|---|
| 201 | const actionsMap: CaseReducers<S, any> = {}
|
|---|
| 202 | const actionMatchers: ActionMatcherDescriptionCollection<S> = []
|
|---|
| 203 | let defaultCaseReducer: CaseReducer<S, Action> | undefined
|
|---|
| 204 | const builder = {
|
|---|
| 205 | addCase(
|
|---|
| 206 | typeOrActionCreator: string | TypedActionCreator<any>,
|
|---|
| 207 | reducer: CaseReducer<S>,
|
|---|
| 208 | ) {
|
|---|
| 209 | if (process.env.NODE_ENV !== 'production') {
|
|---|
| 210 | /*
|
|---|
| 211 | to keep the definition by the user in line with actual behavior,
|
|---|
| 212 | we enforce `addCase` to always be called before calling `addMatcher`
|
|---|
| 213 | as matching cases take precedence over matchers
|
|---|
| 214 | */
|
|---|
| 215 | if (actionMatchers.length > 0) {
|
|---|
| 216 | throw new Error(
|
|---|
| 217 | '`builder.addCase` should only be called before calling `builder.addMatcher`',
|
|---|
| 218 | )
|
|---|
| 219 | }
|
|---|
| 220 | if (defaultCaseReducer) {
|
|---|
| 221 | throw new Error(
|
|---|
| 222 | '`builder.addCase` should only be called before calling `builder.addDefaultCase`',
|
|---|
| 223 | )
|
|---|
| 224 | }
|
|---|
| 225 | }
|
|---|
| 226 | const type =
|
|---|
| 227 | typeof typeOrActionCreator === 'string'
|
|---|
| 228 | ? typeOrActionCreator
|
|---|
| 229 | : typeOrActionCreator.type
|
|---|
| 230 | if (!type) {
|
|---|
| 231 | throw new Error(
|
|---|
| 232 | '`builder.addCase` cannot be called with an empty action type',
|
|---|
| 233 | )
|
|---|
| 234 | }
|
|---|
| 235 | if (type in actionsMap) {
|
|---|
| 236 | throw new Error(
|
|---|
| 237 | '`builder.addCase` cannot be called with two reducers for the same action type ' +
|
|---|
| 238 | `'${type}'`,
|
|---|
| 239 | )
|
|---|
| 240 | }
|
|---|
| 241 | actionsMap[type] = reducer
|
|---|
| 242 | return builder
|
|---|
| 243 | },
|
|---|
| 244 | addAsyncThunk<
|
|---|
| 245 | Returned,
|
|---|
| 246 | ThunkArg,
|
|---|
| 247 | ThunkApiConfig extends AsyncThunkConfig = {},
|
|---|
| 248 | >(
|
|---|
| 249 | asyncThunk: AsyncThunk<Returned, ThunkArg, ThunkApiConfig>,
|
|---|
| 250 | reducers: AsyncThunkReducers<S, ThunkArg, Returned, ThunkApiConfig>,
|
|---|
| 251 | ) {
|
|---|
| 252 | if (process.env.NODE_ENV !== 'production') {
|
|---|
| 253 | // since this uses both action cases and matchers, we can't enforce the order in runtime other than checking for default case
|
|---|
| 254 | if (defaultCaseReducer) {
|
|---|
| 255 | throw new Error(
|
|---|
| 256 | '`builder.addAsyncThunk` should only be called before calling `builder.addDefaultCase`',
|
|---|
| 257 | )
|
|---|
| 258 | }
|
|---|
| 259 | }
|
|---|
| 260 | if (reducers.pending)
|
|---|
| 261 | actionsMap[asyncThunk.pending.type] = reducers.pending
|
|---|
| 262 | if (reducers.rejected)
|
|---|
| 263 | actionsMap[asyncThunk.rejected.type] = reducers.rejected
|
|---|
| 264 | if (reducers.fulfilled)
|
|---|
| 265 | actionsMap[asyncThunk.fulfilled.type] = reducers.fulfilled
|
|---|
| 266 | if (reducers.settled)
|
|---|
| 267 | actionMatchers.push({
|
|---|
| 268 | matcher: asyncThunk.settled,
|
|---|
| 269 | reducer: reducers.settled,
|
|---|
| 270 | })
|
|---|
| 271 | return builder
|
|---|
| 272 | },
|
|---|
| 273 | addMatcher<A>(
|
|---|
| 274 | matcher: TypeGuard<A>,
|
|---|
| 275 | reducer: CaseReducer<S, A extends Action ? A : A & Action>,
|
|---|
| 276 | ) {
|
|---|
| 277 | if (process.env.NODE_ENV !== 'production') {
|
|---|
| 278 | if (defaultCaseReducer) {
|
|---|
| 279 | throw new Error(
|
|---|
| 280 | '`builder.addMatcher` should only be called before calling `builder.addDefaultCase`',
|
|---|
| 281 | )
|
|---|
| 282 | }
|
|---|
| 283 | }
|
|---|
| 284 | actionMatchers.push({ matcher, reducer })
|
|---|
| 285 | return builder
|
|---|
| 286 | },
|
|---|
| 287 | addDefaultCase(reducer: CaseReducer<S, Action>) {
|
|---|
| 288 | if (process.env.NODE_ENV !== 'production') {
|
|---|
| 289 | if (defaultCaseReducer) {
|
|---|
| 290 | throw new Error('`builder.addDefaultCase` can only be called once')
|
|---|
| 291 | }
|
|---|
| 292 | }
|
|---|
| 293 | defaultCaseReducer = reducer
|
|---|
| 294 | return builder
|
|---|
| 295 | },
|
|---|
| 296 | }
|
|---|
| 297 | builderCallback(builder)
|
|---|
| 298 | return [actionsMap, actionMatchers, defaultCaseReducer]
|
|---|
| 299 | }
|
|---|