UNPKG

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