-
-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Expand file tree
/
Copy pathuseSelector.ts
More file actions
209 lines (191 loc) · 6.94 KB
/
Copy pathuseSelector.ts
File metadata and controls
209 lines (191 loc) · 6.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import { useCallback, useDebugValue, useId, useRef, useState } from 'react'
import {
createReduxContextHook,
useReduxContext as useDefaultReduxContext,
} from './useReduxContext'
import { ReactReduxContext } from '../components/Context'
import type { EqualityFn, NoInfer } from '../types'
import type { uSESWS } from '../utils/useSyncExternalStore'
import { notInitialized } from '../utils/useSyncExternalStore'
export type CheckFrequency = 'never' | 'once' | 'always'
interface Measure {
duration: number
startTime: number
detail: { loc: string; trace: string; uid: string; code: string }
}
type Loc = string
type Uid = string
const measurements: Record<Loc, Record<Uid, Measure[]>> = {}
// @ts-ignore
// eslint-disable-next-line no-undef
globalThis[Symbol.for('react-redux-selector-measurements')] = measurements
export interface UseSelectorOptions<Selected = unknown> {
equalityFn?: EqualityFn<Selected>
stabilityCheck?: CheckFrequency
noopCheck?: CheckFrequency
}
export interface UseSelector {
<TState = unknown, Selected = unknown>(
selector: (state: TState) => Selected,
equalityFn?: EqualityFn<Selected>
): Selected
<TState = unknown, Selected = unknown>(
selector: (state: TState) => Selected,
options?: UseSelectorOptions<Selected>
): Selected
}
let useSyncExternalStoreWithSelector = notInitialized as uSESWS
export const initializeUseSelector = (fn: uSESWS) => {
useSyncExternalStoreWithSelector = fn
}
const refEquality: EqualityFn<any> = (a, b) => a === b
/**
* Hook factory, which creates a `useSelector` hook bound to a given context.
*
* @param {React.Context} [context=ReactReduxContext] Context passed to your `<Provider>`.
* @returns {Function} A `useSelector` hook bound to the specified context.
*/
export function createSelectorHook(context = ReactReduxContext): UseSelector {
const useReduxContext =
context === ReactReduxContext
? useDefaultReduxContext
: createReduxContextHook(context)
return function useSelector<TState, Selected extends unknown>(
selector: (state: TState) => Selected,
equalityFnOrOptions:
| EqualityFn<NoInfer<Selected>>
| UseSelectorOptions<NoInfer<Selected>> = {}
): Selected {
const {
equalityFn = refEquality,
stabilityCheck = undefined,
noopCheck = undefined,
} = typeof equalityFnOrOptions === 'function'
? { equalityFn: equalityFnOrOptions }
: equalityFnOrOptions
if (process.env.NODE_ENV !== 'production') {
if (!selector) {
throw new Error(`You must pass a selector to useSelector`)
}
if (typeof selector !== 'function') {
throw new Error(`You must pass a function as a selector to useSelector`)
}
if (typeof equalityFn !== 'function') {
throw new Error(
`You must pass a function as an equality function to useSelector`
)
}
}
const {
store,
subscription,
getServerState,
stabilityCheck: globalStabilityCheck,
noopCheck: globalNoopCheck,
} = useReduxContext()!
const firstRun = useRef(true)
const [trace] = useState<string>(() => {
try {
throw new Error()
} catch (e) {
return e.stack
}
})
const arr = trace.split('\n')
const loc = arr[arr.findIndex((x) => x.startsWith('useSelector')) + 1]
const uid = useId()
const wrappedSelector = useCallback<typeof selector>(
{
[selector.name](state: TState) {
performance.mark('selectorStart')
const selected = selector(state)
performance.mark('selectorEnd')
// @ts-ignore
const measure = performance.measure('selector', {
start: 'selectorStart',
end: 'selectorEnd',
detail: { trace, loc, uid, code: selector.toString() },
}) as any as Measure
;((measurements[loc] ??= {})[uid] ??= []).push(measure)
if (process.env.NODE_ENV !== 'production') {
const finalStabilityCheck =
typeof stabilityCheck === 'undefined'
? globalStabilityCheck
: stabilityCheck
if (
finalStabilityCheck === 'always' ||
(finalStabilityCheck === 'once' && firstRun.current)
) {
const toCompare = selector(state)
if (!equalityFn(selected, toCompare)) {
console.warn(
'Selector ' +
(selector.name || 'unknown') +
' returned a different result when called with the same parameters. This can lead to unnecessary rerenders.' +
'\nSelectors that return a new reference (such as an object or an array) should be memoized: https://redux.js.org/usage/deriving-data-selectors#optimizing-selectors-with-memoization',
{
state,
selected,
selected2: toCompare,
}
)
}
}
const finalNoopCheck =
typeof noopCheck === 'undefined' ? globalNoopCheck : noopCheck
if (
finalNoopCheck === 'always' ||
(finalNoopCheck === 'once' && firstRun.current)
) {
// @ts-ignore
if (selected === state) {
console.warn(
'Selector ' +
(selector.name || 'unknown') +
' returned the root state when called. This can lead to unnecessary rerenders.' +
'\nSelectors that return the entire state are almost certainly a mistake, as they will cause a rerender whenever *anything* in state changes.'
)
}
}
if (firstRun.current) firstRun.current = false
}
return selected
},
}[selector.name],
[selector, globalStabilityCheck, stabilityCheck]
)
const selectedState = useSyncExternalStoreWithSelector(
subscription.addNestedSub,
store.getState,
getServerState || store.getState,
wrappedSelector,
equalityFn
)
useDebugValue(selectedState)
return selectedState
}
}
/**
* A hook to access the redux store's state. This hook takes a selector function
* as an argument. The selector is called with the store state.
*
* This hook takes an optional equality comparison function as the second parameter
* that allows you to customize the way the selected state is compared to determine
* whether the component needs to be re-rendered.
*
* @param {Function} selector the selector function
* @param {Function=} equalityFn the function that will be used to determine equality
*
* @returns {any} the selected state
*
* @example
*
* import React from 'react'
* import { useSelector } from 'react-redux'
*
* export const CounterComponent = () => {
* const counter = useSelector(state => state.counter)
* return <div>{counter}</div>
* }
*/
export const useSelector = /*#__PURE__*/ createSelectorHook()