UNPKG

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