source: node_modules/@reduxjs/toolkit/src/combineSlices.ts@ ba17441

Last change on this file since ba17441 was a762898, checked in by istevanoska <ilinastevanoska@…>, 6 months ago

Added visualizations

  • Property mode set to 100644
File size: 15.8 KB
Line 
1import type {
2 PreloadedStateShapeFromReducersMapObject,
3 Reducer,
4 StateFromReducersMapObject,
5 UnknownAction,
6} from 'redux'
7import { combineReducers } from 'redux'
8import { nanoid } from './nanoid'
9import type {
10 Id,
11 NonUndefined,
12 Tail,
13 UnionToIntersection,
14 WithOptionalProp,
15} from './tsHelpers'
16import { getOrInsertComputed } from './utils'
17
18type SliceLike<ReducerPath extends string, State, PreloadedState = State> = {
19 reducerPath: ReducerPath
20 reducer: Reducer<State, any, PreloadedState>
21}
22
23type AnySliceLike = SliceLike<string, any>
24
25type SliceLikeReducerPath<A extends AnySliceLike> =
26 A extends SliceLike<infer ReducerPath, any> ? ReducerPath : never
27
28type SliceLikeState<A extends AnySliceLike> =
29 A extends SliceLike<any, infer State, any> ? State : never
30
31type SliceLikePreloadedState<A extends AnySliceLike> =
32 A extends SliceLike<any, any, infer PreloadedState> ? PreloadedState : never
33
34export type WithSlice<A extends AnySliceLike> = {
35 [Path in SliceLikeReducerPath<A>]: SliceLikeState<A>
36}
37
38export type WithSlicePreloadedState<A extends AnySliceLike> = {
39 [Path in SliceLikeReducerPath<A>]: SliceLikePreloadedState<A>
40}
41
42type ReducerMap = Record<string, Reducer>
43
44type ExistingSliceLike<DeclaredState, PreloadedState> = {
45 [ReducerPath in keyof DeclaredState]: SliceLike<
46 ReducerPath & string,
47 NonUndefined<DeclaredState[ReducerPath]>,
48 NonUndefined<PreloadedState[ReducerPath & keyof PreloadedState]>
49 >
50}[keyof DeclaredState]
51
52export type InjectConfig = {
53 /**
54 * Allow replacing reducer with a different reference. Normally, an error will be thrown if a different reducer instance to the one already injected is used.
55 */
56 overrideExisting?: boolean
57}
58
59/**
60 * A reducer that allows for slices/reducers to be injected after initialisation.
61 */
62export interface CombinedSliceReducer<
63 InitialState,
64 DeclaredState extends InitialState = InitialState,
65 PreloadedState extends Partial<
66 Record<keyof PreloadedState, any>
67 > = Partial<DeclaredState>,
68> extends Reducer<DeclaredState, UnknownAction, PreloadedState> {
69 /**
70 * Provide a type for slices that will be injected lazily.
71 *
72 * One way to do this would be with interface merging:
73 * ```ts
74 *
75 * export interface LazyLoadedSlices {}
76 *
77 * export const rootReducer = combineSlices(stringSlice).withLazyLoadedSlices<LazyLoadedSlices>();
78 *
79 * // elsewhere
80 *
81 * declare module './reducer' {
82 * export interface LazyLoadedSlices extends WithSlice<typeof booleanSlice> {}
83 * }
84 *
85 * const withBoolean = rootReducer.inject(booleanSlice);
86 *
87 * // elsewhere again
88 *
89 * declare module './reducer' {
90 * export interface LazyLoadedSlices {
91 * customName: CustomState
92 * }
93 * }
94 *
95 * const withCustom = rootReducer.inject({ reducerPath: "customName", reducer: customSlice.reducer })
96 * ```
97 */
98 withLazyLoadedSlices<Lazy = {}, LazyPreloaded = Lazy>(): CombinedSliceReducer<
99 InitialState,
100 Id<DeclaredState & Partial<Lazy>>,
101 Id<PreloadedState & Partial<LazyPreloaded>>
102 >
103
104 /**
105 * Inject a slice.
106 *
107 * Accepts an individual slice, RTKQ API instance, or a "slice-like" { reducerPath, reducer } object.
108 *
109 * ```ts
110 * rootReducer.inject(booleanSlice)
111 * rootReducer.inject(baseApi)
112 * rootReducer.inject({ reducerPath: 'boolean' as const, reducer: newReducer }, { overrideExisting: true })
113 * ```
114 *
115 */
116 inject<Sl extends Id<ExistingSliceLike<DeclaredState, PreloadedState>>>(
117 slice: Sl,
118 config?: InjectConfig,
119 ): CombinedSliceReducer<
120 InitialState,
121 Id<DeclaredState & WithSlice<Sl>>,
122 Id<PreloadedState & Partial<WithSlicePreloadedState<Sl>>>
123 >
124
125 /**
126 * Inject a slice.
127 *
128 * Accepts an individual slice, RTKQ API instance, or a "slice-like" { reducerPath, reducer } object.
129 *
130 * ```ts
131 * rootReducer.inject(booleanSlice)
132 * rootReducer.inject(baseApi)
133 * rootReducer.inject({ reducerPath: 'boolean' as const, reducer: newReducer }, { overrideExisting: true })
134 * ```
135 *
136 */
137 inject<ReducerPath extends string, State, PreloadedState = State>(
138 slice: SliceLike<
139 ReducerPath,
140 State & (ReducerPath extends keyof DeclaredState ? never : State),
141 PreloadedState &
142 (ReducerPath extends keyof PreloadedState ? never : PreloadedState)
143 >,
144 config?: InjectConfig,
145 ): CombinedSliceReducer<
146 InitialState,
147 Id<DeclaredState & WithSlice<SliceLike<ReducerPath, State>>>,
148 Id<
149 PreloadedState &
150 WithSlicePreloadedState<SliceLike<ReducerPath, State, PreloadedState>>
151 >
152 >
153
154 /**
155 * Create a selector that guarantees that the slices injected will have a defined value when selector is run.
156 *
157 * ```ts
158 * const selectBooleanWithoutInjection = (state: RootState) => state.boolean;
159 * // ^? boolean | undefined
160 *
161 * const selectBoolean = rootReducer.inject(booleanSlice).selector((state) => {
162 * // if action hasn't been dispatched since slice was injected, this would usually be undefined
163 * // however selector() uses a Proxy around the first parameter to ensure that it evaluates to the initial state instead, if undefined
164 * return state.boolean;
165 * // ^? boolean
166 * })
167 * ```
168 *
169 * If the reducer is nested inside the root state, a selectState callback can be passed to retrieve the reducer's state.
170 *
171 * ```ts
172 *
173 * export interface LazyLoadedSlices {};
174 *
175 * export const innerReducer = combineSlices(stringSlice).withLazyLoadedSlices<LazyLoadedSlices>();
176 *
177 * export const rootReducer = combineSlices({ inner: innerReducer });
178 *
179 * export type RootState = ReturnType<typeof rootReducer>;
180 *
181 * // elsewhere
182 *
183 * declare module "./reducer.ts" {
184 * export interface LazyLoadedSlices extends WithSlice<typeof booleanSlice> {}
185 * }
186 *
187 * const withBool = innerReducer.inject(booleanSlice);
188 *
189 * const selectBoolean = withBool.selector(
190 * (state) => state.boolean,
191 * (rootState: RootState) => state.inner
192 * );
193 * // now expects to be passed RootState instead of innerReducer state
194 *
195 * ```
196 *
197 * Value passed to selectorFn will be a Proxy - use selector.original(proxy) to get original state value (useful for debugging)
198 *
199 * ```ts
200 * const injectedReducer = rootReducer.inject(booleanSlice);
201 * const selectBoolean = injectedReducer.selector((state) => {
202 * console.log(injectedReducer.selector.original(state).boolean) // possibly undefined
203 * return state.boolean
204 * })
205 * ```
206 */
207 selector: {
208 /**
209 * Create a selector that guarantees that the slices injected will have a defined value when selector is run.
210 *
211 * ```ts
212 * const selectBooleanWithoutInjection = (state: RootState) => state.boolean;
213 * // ^? boolean | undefined
214 *
215 * const selectBoolean = rootReducer.inject(booleanSlice).selector((state) => {
216 * // if action hasn't been dispatched since slice was injected, this would usually be undefined
217 * // however selector() uses a Proxy around the first parameter to ensure that it evaluates to the initial state instead, if undefined
218 * return state.boolean;
219 * // ^? boolean
220 * })
221 * ```
222 *
223 * Value passed to selectorFn will be a Proxy - use selector.original(proxy) to get original state value (useful for debugging)
224 *
225 * ```ts
226 * const injectedReducer = rootReducer.inject(booleanSlice);
227 * const selectBoolean = injectedReducer.selector((state) => {
228 * console.log(injectedReducer.selector.original(state).boolean) // undefined
229 * return state.boolean
230 * })
231 * ```
232 */
233 <Selector extends (state: DeclaredState, ...args: any[]) => unknown>(
234 selectorFn: Selector,
235 ): (
236 state: WithOptionalProp<
237 Parameters<Selector>[0],
238 Exclude<keyof DeclaredState, keyof InitialState>
239 >,
240 ...args: Tail<Parameters<Selector>>
241 ) => ReturnType<Selector>
242
243 /**
244 * Create a selector that guarantees that the slices injected will have a defined value when selector is run.
245 *
246 * ```ts
247 * const selectBooleanWithoutInjection = (state: RootState) => state.boolean;
248 * // ^? boolean | undefined
249 *
250 * const selectBoolean = rootReducer.inject(booleanSlice).selector((state) => {
251 * // if action hasn't been dispatched since slice was injected, this would usually be undefined
252 * // however selector() uses a Proxy around the first parameter to ensure that it evaluates to the initial state instead, if undefined
253 * return state.boolean;
254 * // ^? boolean
255 * })
256 * ```
257 *
258 * If the reducer is nested inside the root state, a selectState callback can be passed to retrieve the reducer's state.
259 *
260 * ```ts
261 *
262 * interface LazyLoadedSlices {};
263 *
264 * const innerReducer = combineSlices(stringSlice).withLazyLoadedSlices<LazyLoadedSlices>();
265 *
266 * const rootReducer = combineSlices({ inner: innerReducer });
267 *
268 * type RootState = ReturnType<typeof rootReducer>;
269 *
270 * // elsewhere
271 *
272 * declare module "./reducer.ts" {
273 * interface LazyLoadedSlices extends WithSlice<typeof booleanSlice> {}
274 * }
275 *
276 * const withBool = innerReducer.inject(booleanSlice);
277 *
278 * const selectBoolean = withBool.selector(
279 * (state) => state.boolean,
280 * (rootState: RootState) => state.inner
281 * );
282 * // now expects to be passed RootState instead of innerReducer state
283 *
284 * ```
285 *
286 * Value passed to selectorFn will be a Proxy - use selector.original(proxy) to get original state value (useful for debugging)
287 *
288 * ```ts
289 * const injectedReducer = rootReducer.inject(booleanSlice);
290 * const selectBoolean = injectedReducer.selector((state) => {
291 * console.log(injectedReducer.selector.original(state).boolean) // possibly undefined
292 * return state.boolean
293 * })
294 * ```
295 */
296 <
297 Selector extends (state: DeclaredState, ...args: any[]) => unknown,
298 RootState,
299 >(
300 selectorFn: Selector,
301 selectState: (
302 rootState: RootState,
303 ...args: Tail<Parameters<Selector>>
304 ) => WithOptionalProp<
305 Parameters<Selector>[0],
306 Exclude<keyof DeclaredState, keyof InitialState>
307 >,
308 ): (
309 state: RootState,
310 ...args: Tail<Parameters<Selector>>
311 ) => ReturnType<Selector>
312 /**
313 * Returns the unproxied state. Useful for debugging.
314 * @param state state Proxy, that ensures injected reducers have value
315 * @returns original, unproxied state
316 * @throws if value passed is not a state Proxy
317 */
318 original: (state: DeclaredState) => InitialState & Partial<DeclaredState>
319 }
320}
321
322type InitialState<Slices extends Array<AnySliceLike | ReducerMap>> =
323 UnionToIntersection<
324 Slices[number] extends infer Slice
325 ? Slice extends AnySliceLike
326 ? WithSlice<Slice>
327 : StateFromReducersMapObject<Slice>
328 : never
329 >
330
331type InitialPreloadedState<Slices extends Array<AnySliceLike | ReducerMap>> =
332 UnionToIntersection<
333 Slices[number] extends infer Slice
334 ? Slice extends AnySliceLike
335 ? WithSlicePreloadedState<Slice>
336 : PreloadedStateShapeFromReducersMapObject<Slice>
337 : never
338 >
339
340const isSliceLike = (
341 maybeSliceLike: AnySliceLike | ReducerMap,
342): maybeSliceLike is AnySliceLike =>
343 'reducerPath' in maybeSliceLike &&
344 typeof maybeSliceLike.reducerPath === 'string'
345
346const getReducers = (slices: Array<AnySliceLike | ReducerMap>) =>
347 slices.flatMap<[string, Reducer]>((sliceOrMap) =>
348 isSliceLike(sliceOrMap)
349 ? [[sliceOrMap.reducerPath, sliceOrMap.reducer]]
350 : Object.entries(sliceOrMap),
351 )
352
353const ORIGINAL_STATE = Symbol.for('rtk-state-proxy-original')
354
355const isStateProxy = (value: any) => !!value && !!value[ORIGINAL_STATE]
356
357const stateProxyMap = new WeakMap<object, object>()
358
359const createStateProxy = <State extends object>(
360 state: State,
361 reducerMap: Partial<Record<PropertyKey, Reducer>>,
362 initialStateCache: Record<PropertyKey, unknown>,
363) =>
364 getOrInsertComputed(
365 stateProxyMap,
366 state,
367 () =>
368 new Proxy(state, {
369 get: (target, prop, receiver) => {
370 if (prop === ORIGINAL_STATE) return target
371 const result = Reflect.get(target, prop, receiver)
372 if (typeof result === 'undefined') {
373 const cached = initialStateCache[prop]
374 if (typeof cached !== 'undefined') return cached
375 const reducer = reducerMap[prop]
376 if (reducer) {
377 // ensure action type is random, to prevent reducer treating it differently
378 const reducerResult = reducer(undefined, { type: nanoid() })
379 if (typeof reducerResult === 'undefined') {
380 throw new Error(
381 `The slice reducer for key "${prop.toString()}" returned undefined when called for selector(). ` +
382 `If the state passed to the reducer is undefined, you must ` +
383 `explicitly return the initial state. The initial state may ` +
384 `not be undefined. If you don't want to set a value for this reducer, ` +
385 `you can use null instead of undefined.`,
386 )
387 }
388 initialStateCache[prop] = reducerResult
389 return reducerResult
390 }
391 }
392 return result
393 },
394 }),
395 ) as State
396
397const original = (state: any) => {
398 if (!isStateProxy(state)) {
399 throw new Error('original must be used on state Proxy')
400 }
401 return state[ORIGINAL_STATE]
402}
403
404const emptyObject = {}
405const noopReducer: Reducer<Record<string, any>> = (state = emptyObject) => state
406
407export function combineSlices<Slices extends Array<AnySliceLike | ReducerMap>>(
408 ...slices: Slices
409): CombinedSliceReducer<
410 Id<InitialState<Slices>>,
411 Id<InitialState<Slices>>,
412 Partial<Id<InitialPreloadedState<Slices>>>
413> {
414 const reducerMap = Object.fromEntries(getReducers(slices))
415
416 const getReducer = () =>
417 Object.keys(reducerMap).length ? combineReducers(reducerMap) : noopReducer
418
419 let reducer = getReducer()
420
421 function combinedReducer(
422 state: Record<string, unknown>,
423 action: UnknownAction,
424 ) {
425 return reducer(state, action)
426 }
427
428 combinedReducer.withLazyLoadedSlices = () => combinedReducer
429
430 const initialStateCache: Record<PropertyKey, unknown> = {}
431
432 const inject = (
433 slice: AnySliceLike,
434 config: InjectConfig = {},
435 ): typeof combinedReducer => {
436 const { reducerPath, reducer: reducerToInject } = slice
437
438 const currentReducer = reducerMap[reducerPath]
439 if (
440 !config.overrideExisting &&
441 currentReducer &&
442 currentReducer !== reducerToInject
443 ) {
444 if (
445 typeof process !== 'undefined' &&
446 process.env.NODE_ENV === 'development'
447 ) {
448 console.error(
449 `called \`inject\` to override already-existing reducer ${reducerPath} without specifying \`overrideExisting: true\``,
450 )
451 }
452
453 return combinedReducer
454 }
455
456 if (config.overrideExisting && currentReducer !== reducerToInject) {
457 delete initialStateCache[reducerPath]
458 }
459
460 reducerMap[reducerPath] = reducerToInject
461
462 reducer = getReducer()
463
464 return combinedReducer
465 }
466
467 const selector = Object.assign(
468 function makeSelector<State extends object, RootState, Args extends any[]>(
469 selectorFn: (state: State, ...args: Args) => any,
470 selectState?: (rootState: RootState, ...args: Args) => State,
471 ) {
472 return function selector(state: State, ...args: Args) {
473 return selectorFn(
474 createStateProxy(
475 selectState ? selectState(state as any, ...args) : state,
476 reducerMap,
477 initialStateCache,
478 ),
479 ...args,
480 )
481 }
482 },
483 { original },
484 )
485
486 return Object.assign(combinedReducer, { inject, selector }) as any
487}
Note: See TracBrowser for help on using the repository browser.