UNPKG

8.64 kBPlain TextView Raw
1import { Reducer } from 'redux'
2import {
3 ActionCreatorWithoutPayload,
4 createAction,
5 PayloadAction,
6 PayloadActionCreator,
7 PrepareAction,
8 _ActionCreatorWithPreparedPayload
9} from './createAction'
10import { CaseReducer, CaseReducers, createReducer } from './createReducer'
11import {
12 ActionReducerMapBuilder,
13 executeReducerBuilderCallback
14} from './mapBuilders'
15import { NoInfer } from './tsHelpers'
16
17/**
18 * An action creator attached to a slice.
19 *
20 * @deprecated please use PayloadActionCreator directly
21 *
22 * @public
23 */
24export type SliceActionCreator<P> = PayloadActionCreator<P>
25
26/**
27 * The return value of `createSlice`
28 *
29 * @public
30 */
31export interface Slice<
32 State = any,
33 CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
34 Name extends string = string
35> {
36 /**
37 * The slice name.
38 */
39 name: Name
40
41 /**
42 * The slice's reducer.
43 */
44 reducer: Reducer<State>
45
46 /**
47 * Action creators for the types of actions that are handled by the slice
48 * reducer.
49 */
50 actions: CaseReducerActions<CaseReducers>
51
52 /**
53 * The individual case reducer functions that were passed in the `reducers` parameter.
54 * This enables reuse and testing if they were defined inline when calling `createSlice`.
55 */
56 caseReducers: SliceDefinedCaseReducers<CaseReducers>
57}
58
59/**
60 * Options for `createSlice()`.
61 *
62 * @public
63 */
64export interface CreateSliceOptions<
65 State = any,
66 CR extends SliceCaseReducers<State> = SliceCaseReducers<State>,
67 Name extends string = string
68> {
69 /**
70 * The slice's name. Used to namespace the generated action types.
71 */
72 name: Name
73
74 /**
75 * The initial state to be returned by the slice reducer.
76 */
77 initialState: State
78
79 /**
80 * A mapping from action types to action-type-specific *case reducer*
81 * functions. For every action type, a matching action creator will be
82 * generated using `createAction()`.
83 */
84 reducers: ValidateSliceCaseReducers<State, CR>
85
86 /**
87 * A callback that receives a *builder* object to define
88 * case reducers via calls to `builder.addCase(actionCreatorOrType, reducer)`.
89 *
90 * Alternatively, a mapping from action types to action-type-specific *case reducer*
91 * functions. These reducers should have existing action types used
92 * as the keys, and action creators will _not_ be generated.
93 *
94 * @example
95```ts
96import { createAction, createSlice, Action, AnyAction } from '@reduxjs/toolkit'
97const incrementBy = createAction<number>('incrementBy')
98const decrement = createAction('decrement')
99
100interface RejectedAction extends Action {
101 error: Error
102}
103
104function isRejectedAction(action: AnyAction): action is RejectedAction {
105 return action.type.endsWith('rejected')
106}
107
108createSlice({
109 name: 'counter',
110 initialState: 0,
111 reducers: {},
112 extraReducers: builder => {
113 builder
114 .addCase(incrementBy, (state, action) => {
115 // action is inferred correctly here if using TS
116 })
117 // You can chain calls, or have separate `builder.addCase()` lines each time
118 .addCase(decrement, (state, action) => {})
119 // You can match a range of action types
120 .addMatcher(
121 isRejectedAction,
122 // `action` will be inferred as a RejectedAction due to isRejectedAction being defined as a type guard
123 (state, action) => {}
124 )
125 // and provide a default case if no other handlers matched
126 .addDefaultCase((state, action) => {})
127 }
128})
129```
130 */
131 extraReducers?:
132 | CaseReducers<NoInfer<State>, any>
133 | ((builder: ActionReducerMapBuilder<NoInfer<State>>) => void)
134}
135
136/**
137 * A CaseReducer with a `prepare` method.
138 *
139 * @public
140 */
141export type CaseReducerWithPrepare<State, Action extends PayloadAction> = {
142 reducer: CaseReducer<State, Action>
143 prepare: PrepareAction<Action['payload']>
144}
145
146/**
147 * The type describing a slice's `reducers` option.
148 *
149 * @public
150 */
151export type SliceCaseReducers<State> = {
152 [K: string]:
153 | CaseReducer<State, PayloadAction<any>>
154 | CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>
155}
156
157/**
158 * Derives the slice's `actions` property from the `reducers` options
159 *
160 * @public
161 */
162export type CaseReducerActions<CaseReducers extends SliceCaseReducers<any>> = {
163 [Type in keyof CaseReducers]: CaseReducers[Type] extends { prepare: any }
164 ? ActionCreatorForCaseReducerWithPrepare<CaseReducers[Type]>
165 : ActionCreatorForCaseReducer<CaseReducers[Type]>
166}
167
168/**
169 * Get a `PayloadActionCreator` type for a passed `CaseReducerWithPrepare`
170 *
171 * @internal
172 */
173type ActionCreatorForCaseReducerWithPrepare<
174 CR extends { prepare: any }
175> = _ActionCreatorWithPreparedPayload<CR['prepare'], string>
176
177/**
178 * Get a `PayloadActionCreator` type for a passed `CaseReducer`
179 *
180 * @internal
181 */
182type ActionCreatorForCaseReducer<CR> = CR extends (
183 state: any,
184 action: infer Action
185) => any
186 ? Action extends { payload: infer P }
187 ? PayloadActionCreator<P>
188 : ActionCreatorWithoutPayload
189 : ActionCreatorWithoutPayload
190
191/**
192 * Extracts the CaseReducers out of a `reducers` object, even if they are
193 * tested into a `CaseReducerWithPrepare`.
194 *
195 * @internal
196 */
197type SliceDefinedCaseReducers<CaseReducers extends SliceCaseReducers<any>> = {
198 [Type in keyof CaseReducers]: CaseReducers[Type] extends {
199 reducer: infer Reducer
200 }
201 ? Reducer
202 : CaseReducers[Type]
203}
204
205/**
206 * Used on a SliceCaseReducers object.
207 * Ensures that if a CaseReducer is a `CaseReducerWithPrepare`, that
208 * the `reducer` and the `prepare` function use the same type of `payload`.
209 *
210 * Might do additional such checks in the future.
211 *
212 * This type is only ever useful if you want to write your own wrapper around
213 * `createSlice`. Please don't use it otherwise!
214 *
215 * @public
216 */
217export type ValidateSliceCaseReducers<
218 S,
219 ACR extends SliceCaseReducers<S>
220> = ACR &
221 {
222 [T in keyof ACR]: ACR[T] extends {
223 reducer(s: S, action?: infer A): any
224 }
225 ? {
226 prepare(...a: never[]): Omit<A, 'type'>
227 }
228 : {}
229 }
230
231function getType(slice: string, actionKey: string): string {
232 return `${slice}/${actionKey}`
233}
234
235/**
236 * A function that accepts an initial state, an object full of reducer
237 * functions, and a "slice name", and automatically generates
238 * action creators and action types that correspond to the
239 * reducers and state.
240 *
241 * The `reducer` argument is passed to `createReducer()`.
242 *
243 * @public
244 */
245export function createSlice<
246 State,
247 CaseReducers extends SliceCaseReducers<State>,
248 Name extends string = string
249>(
250 options: CreateSliceOptions<State, CaseReducers, Name>
251): Slice<State, CaseReducers, Name> {
252 const { name, initialState } = options
253 if (!name) {
254 throw new Error('`name` is a required option for createSlice')
255 }
256 const reducers = options.reducers || {}
257 const [
258 extraReducers = {},
259 actionMatchers = [],
260 defaultCaseReducer = undefined
261 ] =
262 typeof options.extraReducers === 'undefined'
263 ? []
264 : typeof options.extraReducers === 'function'
265 ? executeReducerBuilderCallback(options.extraReducers)
266 : [options.extraReducers]
267
268 const reducerNames = Object.keys(reducers)
269
270 const sliceCaseReducersByName: Record<string, CaseReducer> = {}
271 const sliceCaseReducersByType: Record<string, CaseReducer> = {}
272 const actionCreators: Record<string, Function> = {}
273
274 reducerNames.forEach(reducerName => {
275 const maybeReducerWithPrepare = reducers[reducerName]
276 const type = getType(name, reducerName)
277
278 let caseReducer: CaseReducer<State, any>
279 let prepareCallback: PrepareAction<any> | undefined
280
281 if ('reducer' in maybeReducerWithPrepare) {
282 caseReducer = maybeReducerWithPrepare.reducer
283 prepareCallback = maybeReducerWithPrepare.prepare
284 } else {
285 caseReducer = maybeReducerWithPrepare
286 }
287
288 sliceCaseReducersByName[reducerName] = caseReducer
289 sliceCaseReducersByType[type] = caseReducer
290 actionCreators[reducerName] = prepareCallback
291 ? createAction(type, prepareCallback)
292 : createAction(type)
293 })
294
295 const finalCaseReducers = { ...extraReducers, ...sliceCaseReducersByType }
296 const reducer = createReducer(
297 initialState,
298 finalCaseReducers as any,
299 actionMatchers,
300 defaultCaseReducer
301 )
302
303 return {
304 name,
305 reducer,
306 actions: actionCreators as any,
307 caseReducers: sliceCaseReducersByName as any
308 }
309}