-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
Copy pathmapBuilders.ts
196 lines (186 loc) · 6.75 KB
/
mapBuilders.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import type { Action, AnyAction } from 'redux'
import type {
CaseReducer,
CaseReducers,
ActionMatcherDescriptionCollection,
} from './createReducer'
import type { TypeGuard } from './tsHelpers'
export interface TypedActionCreator<Type extends string> {
(...args: any[]): Action<Type>
type: Type
}
/**
* A builder for an action <-> reducer map.
*
* @public
*/
export interface ActionReducerMapBuilder<State> {
/**
* Adds a case reducer to handle a single exact action type.
* @remarks
* All calls to `builder.addCase` must come before any calls to `builder.addMatcher` or `builder.addDefaultCase`.
* @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.
* @param reducer - The actual case reducer function.
*/
addCase<ActionCreator extends TypedActionCreator<string>>(
actionCreator: ActionCreator,
reducer: CaseReducer<State, ReturnType<ActionCreator>>
): ActionReducerMapBuilder<State>
/**
* Adds a case reducer to handle a single exact action type.
* @remarks
* All calls to `builder.addCase` must come before any calls to `builder.addMatcher` or `builder.addDefaultCase`.
* @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.
* @param reducer - The actual case reducer function.
*/
addCase<Type extends string, A extends Action<Type>>(
type: Type,
reducer: CaseReducer<State, A>
): ActionReducerMapBuilder<State>
/**
* Allows you to match your incoming actions against your own filter function instead of only the `action.type` property.
* @remarks
* If multiple matcher reducers match, all of them will be executed in the order
* they were defined in - even if a case reducer already matched.
* All calls to `builder.addMatcher` must come after any calls to `builder.addCase` and before any calls to `builder.addDefaultCase`.
* @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)
* function
* @param reducer - The actual case reducer function.
*
* @example
```ts
import {
createAction,
createReducer,
AsyncThunk,
AnyAction,
} from "@reduxjs/toolkit";
type GenericAsyncThunk = AsyncThunk<unknown, unknown, any>;
type PendingAction = ReturnType<GenericAsyncThunk["pending"]>;
type RejectedAction = ReturnType<GenericAsyncThunk["rejected"]>;
type FulfilledAction = ReturnType<GenericAsyncThunk["fulfilled"]>;
const initialState: Record<string, string> = {};
const resetAction = createAction("reset-tracked-loading-state");
function isPendingAction(action: AnyAction): action is PendingAction {
return action.type.endsWith("/pending");
}
const reducer = createReducer(initialState, (builder) => {
builder
.addCase(resetAction, () => initialState)
// matcher can be defined outside as a type predicate function
.addMatcher(isPendingAction, (state, action) => {
state[action.meta.requestId] = "pending";
})
.addMatcher(
// matcher can be defined inline as a type predicate function
(action): action is RejectedAction => action.type.endsWith("/rejected"),
(state, action) => {
state[action.meta.requestId] = "rejected";
}
)
// matcher can just return boolean and the matcher can receive a generic argument
.addMatcher<FulfilledAction>(
(action) => action.type.endsWith("/fulfilled"),
(state, action) => {
state[action.meta.requestId] = "fulfilled";
}
);
});
```
*/
addMatcher<A>(
matcher: TypeGuard<A> | ((action: any) => boolean),
reducer: CaseReducer<State, A extends AnyAction ? A : A & AnyAction>
): Omit<ActionReducerMapBuilder<State>, 'addCase'>
/**
* Adds a "default case" reducer that is executed if no case reducer and no matcher
* reducer was executed for this action.
* @param reducer - The fallback "default case" reducer function.
*
* @example
```ts
import { createReducer } from '@reduxjs/toolkit'
const initialState = { otherActions: 0 }
const reducer = createReducer(initialState, builder => {
builder
// .addCase(...)
// .addMatcher(...)
.addDefaultCase((state, action) => {
state.otherActions++
})
})
```
*/
addDefaultCase(reducer: CaseReducer<State, AnyAction>): {}
}
export function executeReducerBuilderCallback<S>(
builderCallback: (builder: ActionReducerMapBuilder<S>) => void
): [
CaseReducers<S, any>,
ActionMatcherDescriptionCollection<S>,
CaseReducer<S, AnyAction> | undefined
] {
const actionsMap: CaseReducers<S, any> = {}
const actionMatchers: ActionMatcherDescriptionCollection<S> = []
let defaultCaseReducer: CaseReducer<S, AnyAction> | undefined
const builder = {
addCase(
typeOrActionCreator: string | TypedActionCreator<any>,
reducer: CaseReducer<S>
) {
if (process.env.NODE_ENV !== 'production') {
/*
to keep the definition by the user in line with actual behavior,
we enforce `addCase` to always be called before calling `addMatcher`
as matching cases take precedence over matchers
*/
if (actionMatchers.length > 0) {
throw new Error(
'`builder.addCase` should only be called before calling `builder.addMatcher`'
)
}
if (defaultCaseReducer) {
throw new Error(
'`builder.addCase` should only be called before calling `builder.addDefaultCase`'
)
}
}
const type =
typeof typeOrActionCreator === 'string'
? typeOrActionCreator
: typeOrActionCreator.type
if (type in actionsMap) {
throw new Error(
'addCase cannot be called with two reducers for the same action type'
)
}
actionsMap[type] = reducer
return builder
},
addMatcher<A>(
matcher: TypeGuard<A>,
reducer: CaseReducer<S, A extends AnyAction ? A : A & AnyAction>
) {
if (process.env.NODE_ENV !== 'production') {
if (defaultCaseReducer) {
throw new Error(
'`builder.addMatcher` should only be called before calling `builder.addDefaultCase`'
)
}
}
actionMatchers.push({ matcher, reducer })
return builder
},
addDefaultCase(reducer: CaseReducer<S, AnyAction>) {
if (process.env.NODE_ENV !== 'production') {
if (defaultCaseReducer) {
throw new Error('`builder.addDefaultCase` can only be called once')
}
}
defaultCaseReducer = reducer
return builder
},
}
builderCallback(builder)
return [actionsMap, actionMatchers, defaultCaseReducer]
}