UNPKG

8.69 kBPlain TextView Raw
1import type {
2 Selector,
3 GetParamsFromSelectors,
4 OutputSelector,
5 SelectorArray,
6 SelectorResultArray,
7 DropFirst,
8 MergeParameters,
9 Expand,
10 ObjValueTuple,
11 Head,
12 Tail
13} from './types'
14
15export type {
16 Selector,
17 GetParamsFromSelectors,
18 GetStateFromSelectors,
19 OutputSelector,
20 EqualityFn,
21 SelectorArray,
22 SelectorResultArray,
23 ParametricSelector,
24 OutputParametricSelector,
25 OutputSelectorFields
26} from './types'
27
28import {
29 defaultMemoize,
30 defaultEqualityCheck,
31 DefaultMemoizeOptions
32} from './defaultMemoize'
33
34export { defaultMemoize, defaultEqualityCheck }
35
36export type { DefaultMemoizeOptions }
37
38function getDependencies(funcs: unknown[]) {
39 const dependencies = Array.isArray(funcs[0]) ? funcs[0] : funcs
40
41 if (!dependencies.every(dep => typeof dep === 'function')) {
42 const dependencyTypes = dependencies
43 .map(dep =>
44 typeof dep === 'function'
45 ? `function ${dep.name || 'unnamed'}()`
46 : typeof dep
47 )
48 .join(', ')
49
50 throw new Error(
51 `createSelector expects all input-selectors to be functions, but received the following types: [${dependencyTypes}]`
52 )
53 }
54
55 return dependencies as SelectorArray
56}
57
58export function createSelectorCreator<
59 /** Selectors will eventually accept some function to be memoized */
60 F extends (...args: unknown[]) => unknown,
61 /** A memoizer such as defaultMemoize that accepts a function + some possible options */
62 MemoizeFunction extends (func: F, ...options: any[]) => F,
63 /** The additional options arguments to the memoizer */
64 MemoizeOptions extends unknown[] = DropFirst<Parameters<MemoizeFunction>>
65>(
66 memoize: MemoizeFunction,
67 ...memoizeOptionsFromArgs: DropFirst<Parameters<MemoizeFunction>>
68) {
69 const createSelector = (...funcs: Function[]) => {
70 let recomputations = 0
71 let lastResult: unknown
72
73 // Due to the intricacies of rest params, we can't do an optional arg after `...funcs`.
74 // So, start by declaring the default value here.
75 // (And yes, the words 'memoize' and 'options' appear too many times in this next sequence.)
76 let directlyPassedOptions: CreateSelectorOptions<MemoizeOptions> = {
77 memoizeOptions: undefined
78 }
79
80 // Normally, the result func or "output selector" is the last arg
81 let resultFunc = funcs.pop()
82
83 // If the result func is actually an _object_, assume it's our options object
84 if (typeof resultFunc === 'object') {
85 directlyPassedOptions = resultFunc as any
86 // and pop the real result func off
87 resultFunc = funcs.pop()
88 }
89
90 if (typeof resultFunc !== 'function') {
91 throw new Error(
92 `createSelector expects an output function after the inputs, but received: [${typeof resultFunc}]`
93 )
94 }
95
96 // Determine which set of options we're using. Prefer options passed directly,
97 // but fall back to options given to createSelectorCreator.
98 const { memoizeOptions = memoizeOptionsFromArgs } = directlyPassedOptions
99
100 // Simplifying assumption: it's unlikely that the first options arg of the provided memoizer
101 // is an array. In most libs I've looked at, it's an equality function or options object.
102 // Based on that, if `memoizeOptions` _is_ an array, we assume it's a full
103 // user-provided array of options. Otherwise, it must be just the _first_ arg, and so
104 // we wrap it in an array so we can apply it.
105 const finalMemoizeOptions = Array.isArray(memoizeOptions)
106 ? memoizeOptions
107 : ([memoizeOptions] as MemoizeOptions)
108
109 const dependencies = getDependencies(funcs)
110
111 const memoizedResultFunc = memoize(
112 function recomputationWrapper() {
113 recomputations++
114 // apply arguments instead of spreading for performance.
115 return resultFunc!.apply(null, arguments)
116 } as F,
117 ...finalMemoizeOptions
118 )
119
120 // If a selector is called with the exact same arguments we don't need to traverse our dependencies again.
121 const selector = memoize(function dependenciesChecker() {
122 const params = []
123 const length = dependencies.length
124
125 for (let i = 0; i < length; i++) {
126 // apply arguments instead of spreading and mutate a local list of params for performance.
127 // @ts-ignore
128 params.push(dependencies[i].apply(null, arguments))
129 }
130
131 // apply arguments instead of spreading for performance.
132 lastResult = memoizedResultFunc.apply(null, params)
133 return lastResult
134 } as F)
135
136 Object.assign(selector, {
137 resultFunc,
138 memoizedResultFunc,
139 dependencies,
140 lastResult: () => lastResult,
141 recomputations: () => recomputations,
142 resetRecomputations: () => (recomputations = 0)
143 })
144
145 return selector
146 }
147 // @ts-ignore
148 return createSelector as CreateSelectorFunction<
149 F,
150 MemoizeFunction,
151 MemoizeOptions
152 >
153}
154
155export interface CreateSelectorOptions<MemoizeOptions extends unknown[]> {
156 memoizeOptions: MemoizeOptions[0] | MemoizeOptions
157}
158
159/**
160 * An instance of createSelector, customized with a given memoize implementation
161 */
162export interface CreateSelectorFunction<
163 F extends (...args: unknown[]) => unknown,
164 MemoizeFunction extends (func: F, ...options: any[]) => F,
165 MemoizeOptions extends unknown[] = DropFirst<Parameters<MemoizeFunction>>,
166 Keys = Expand<
167 Pick<ReturnType<MemoizeFunction>, keyof ReturnType<MemoizeFunction>>
168 >
169> {
170 /** Input selectors as separate inline arguments */
171 <Selectors extends SelectorArray, Result>(
172 ...items: [
173 ...Selectors,
174 (...args: SelectorResultArray<Selectors>) => Result
175 ]
176 ): OutputSelector<
177 Selectors,
178 Result,
179 (...args: SelectorResultArray<Selectors>) => Result,
180 GetParamsFromSelectors<Selectors>,
181 Keys
182 > &
183 Keys
184
185 /** Input selectors as separate inline arguments with memoizeOptions passed */
186 <Selectors extends SelectorArray, Result>(
187 ...items: [
188 ...Selectors,
189 (...args: SelectorResultArray<Selectors>) => Result,
190 CreateSelectorOptions<MemoizeOptions>
191 ]
192 ): OutputSelector<
193 Selectors,
194 Result,
195 ((...args: SelectorResultArray<Selectors>) => Result),
196 GetParamsFromSelectors<Selectors>,
197 Keys
198 > &
199 Keys
200
201 /** Input selectors as a separate array */
202 <Selectors extends SelectorArray, Result>(
203 selectors: [...Selectors],
204 combiner: (...args: SelectorResultArray<Selectors>) => Result,
205 options?: CreateSelectorOptions<MemoizeOptions>
206 ): OutputSelector<
207 Selectors,
208 Result,
209 (...args: SelectorResultArray<Selectors>) => Result,
210 GetParamsFromSelectors<Selectors>,
211 Keys
212 > &
213 Keys
214}
215
216export const createSelector =
217 /* #__PURE__ */ createSelectorCreator(defaultMemoize)
218
219type SelectorsObject = { [key: string]: (...args: any[]) => any }
220
221export interface StructuredSelectorCreator {
222 <
223 SelectorMap extends SelectorsObject,
224 SelectorParams = MergeParameters<ObjValueTuple<SelectorMap>>
225 >(
226 selectorMap: SelectorMap,
227 selectorCreator?: CreateSelectorFunction<any, any, any>
228 ): (
229 // Accept an arbitrary number of parameters for all selectors
230 // The annoying head/tail bit here is because TS isn't convinced that
231 // the `SelectorParams` type is really an array, so we launder things.
232 // Plus it matches common usage anyway.
233 state: Head<SelectorParams>,
234 ...params: Tail<SelectorParams>
235 ) => {
236 [Key in keyof SelectorMap]: ReturnType<SelectorMap[Key]>
237 }
238
239 <State, Result = State>(
240 selectors: { [K in keyof Result]: Selector<State, Result[K], never> },
241 selectorCreator?: CreateSelectorFunction<any, any, any>
242 ): Selector<State, Result, never>
243}
244
245// Manual definition of state and output arguments
246export const createStructuredSelector = ((
247 selectors: SelectorsObject,
248 selectorCreator = createSelector
249) => {
250 if (typeof selectors !== 'object') {
251 throw new Error(
252 'createStructuredSelector expects first argument to be an object ' +
253 `where each property is a selector, instead received a ${typeof selectors}`
254 )
255 }
256 const objectKeys = Object.keys(selectors)
257 const resultSelector = selectorCreator(
258 // @ts-ignore
259 objectKeys.map(key => selectors[key]),
260 (...values: any[]) => {
261 return values.reduce((composition, value, index) => {
262 composition[objectKeys[index]] = value
263 return composition
264 }, {})
265 }
266 )
267 return resultSelector
268}) as unknown as StructuredSelectorCreator