Skip to content

Commit 6007055

Browse files
authored
Merge pull request #2000 from reduxjs/selector-check
2 parents b5a6d14 + b6cbd2d commit 6007055

File tree

8 files changed

+393
-58
lines changed

8 files changed

+393
-58
lines changed

docs/api/Provider.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ interface ProviderProps<A extends Action = AnyAction, S = any> {
4343
*/
4444
context?: Context<ReactReduxContextValue<S, A>>
4545

46+
/** Global configuration for the `useSelector` stability check */
47+
stabilityCheck?: StabilityCheck
48+
4649
/** The top-level React elements in your component tree, such as `<App />` **/
4750
children: ReactNode
4851
}

docs/api/hooks.md

Lines changed: 99 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,36 +44,49 @@ From there, you may import any of the listed React Redux hooks APIs and use them
4444

4545
## `useSelector()`
4646

47-
```js
48-
const result: any = useSelector(selector: Function, equalityFn?: Function)
47+
```ts
48+
type RootState = ReturnType<typeof store.getState>
49+
type SelectorFn = <Selected>(state: RootState) => Selected
50+
type EqualityFn = (a: any, b: any) => boolean
51+
export type StabilityCheck = 'never' | 'once' | 'always'
52+
53+
interface UseSelectorOptions {
54+
equalityFn?: EqualityFn
55+
stabilityCheck?: StabilityCheck
56+
}
57+
58+
const result: Selected = useSelector(
59+
selector: SelectorFunction,
60+
options?: EqualityFn | UseSelectorOptions
61+
)
4962
```
5063

51-
Allows you to extract data from the Redux store state, using a selector function.
64+
Allows you to extract data from the Redux store state for use in this component, using a selector function.
5265

5366
:::info
5467

5568
The selector function should be [pure](https://en.wikipedia.org/wiki/Pure_function) since it is potentially executed multiple times and at arbitrary points in time.
5669

70+
See [Using Redux: Deriving Data with Selectors](https://redux.js.org/usage/deriving-data-selectors) in the Redux docs for more details on writing and using selector functions.
71+
5772
:::
5873

59-
The selector is approximately equivalent to the [`mapStateToProps` argument to `connect`](../using-react-redux/connect-extracting-data-with-mapStateToProps.md) conceptually. The selector will be called with the entire Redux store state as its only argument. The selector will be run whenever the function component renders (unless its reference hasn't changed since a previous render of the component so that a cached result can be returned by the hook without re-running the selector). `useSelector()` will also subscribe to the Redux store, and run your selector whenever an action is dispatched.
74+
The selector will be called with the entire Redux store state as its only argument. The selector may return any value as a result, including directly returning a value that was nested inside `state`, or deriving new values. The return value of the selector will be used as the return value of the `useSelector()` hook.
6075

61-
However, there are some differences between the selectors passed to `useSelector()` and a `mapState` function:
76+
The selector will be run whenever the function component renders (unless its reference hasn't changed since a previous render of the component so that a cached result can be returned by the hook without re-running the selector). `useSelector()` will also subscribe to the Redux store, and run your selector whenever an action is dispatched.
6277

63-
- The selector may return any value as a result, not just an object. The return value of the selector will be used as the return value of the `useSelector()` hook.
64-
- When an action is dispatched, `useSelector()` will do a reference comparison of the previous selector result value and the current result value. If they are different, the component will be forced to re-render. If they are the same, the component will not re-render.
65-
- The selector function does _not_ receive an `ownProps` argument. However, props can be used through closure (see the examples below) or by using a curried selector.
66-
- Extra care must be taken when using memoizing selectors (see examples below for more details).
67-
- `useSelector()` uses strict `===` reference equality checks by default, not shallow equality (see the following section for more details).
78+
When an action is dispatched, `useSelector()` will do a reference comparison of the previous selector result value and the current result value. If they are different, the component will be forced to re-render. If they are the same, the component will not re-render. `useSelector()` uses strict `===` reference equality checks by default, not shallow equality (see the following section for more details).
79+
80+
The selector is approximately equivalent to the [`mapStateToProps` argument to `connect`](../using-react-redux/connect-extracting-data-with-mapStateToProps.md) conceptually.
81+
82+
You may call `useSelector()` multiple times within a single function component. Each call to `useSelector()` creates an individual subscription to the Redux store. Because of the React update batching behavior used in React Redux v7, a dispatched action that causes multiple `useSelector()`s in the same component to return new values _should_ only result in a single re-render.
6883

6984
:::info
7085

7186
There are potential edge cases with using props in selectors that may cause issues. See the [Usage Warnings](#usage-warnings) section of this page for further details.
7287

7388
:::
7489

75-
You may call `useSelector()` multiple times within a single function component. Each call to `useSelector()` creates an individual subscription to the Redux store. Because of the React update batching behavior used in React Redux v7, a dispatched action that causes multiple `useSelector()`s in the same component to return new values _should_ only result in a single re-render.
76-
7790
### Equality Comparisons and Updates
7891

7992
When the function component renders, the provided selector function will be called and its result will be returned
@@ -96,8 +109,13 @@ every time will _always_ force a re-render by default. If you want to retrieve m
96109
```js
97110
import { shallowEqual, useSelector } from 'react-redux'
98111

99-
// later
112+
// Pass it as the second argument directly
100113
const selectedData = useSelector(selectorReturningObject, shallowEqual)
114+
115+
// or pass it as the `equalityFn` field in the options argument
116+
const selectedData = useSelector(selectorReturningObject, {
117+
equalityFn: shallowEqual,
118+
})
101119
```
102120

103121
- Use a custom equality function as the `equalityFn` argument to `useSelector()`, like:
@@ -240,10 +258,64 @@ export const App = () => {
240258
}
241259
```
242260

261+
### Development mode checks
262+
263+
`useSelector` runs some extra checks in development mode to watch for unexpected behavior. These checks do not run in production builds.
264+
265+
:::info
266+
267+
These checks were first added in v8.1.0
268+
269+
:::
270+
271+
#### Selector result stability
272+
273+
In development, the provided selector function is run an extra time with the same parameter during the first call to `useSelector`, and warns in the console if the selector returns a different result (based on the `equalityFn` provided).
274+
275+
This is important, as a selector returning that returns a different result reference with the same parameter will cause unnecessary rerenders.
276+
277+
```ts
278+
// this selector will return a new object reference whenever called,
279+
// which causes the component to rerender after *every* action is dispatched
280+
const { count, user } = useSelector((state) => ({
281+
count: state.count,
282+
user: state.user,
283+
}))
284+
```
285+
286+
If a selector result is suitably stable (or the selector is memoized), it will not return a different result and no warning will be logged.
287+
288+
By default, this will only happen when the selector is first called. You can configure the check in the Provider or at each `useSelector` call.
289+
290+
```tsx title="Global setting via context"
291+
<Provider store={store} stabilityCheck="always">
292+
{children}
293+
</Provider>
294+
```
295+
296+
```tsx title="Individual hook setting"
297+
function Component() {
298+
const count = useSelector(selectCount, { stabilityCheck: 'never' })
299+
// run once (default)
300+
const user = useSelector(selectUser, { stabilityCheck: 'once' })
301+
// ...
302+
}
303+
```
304+
305+
### Comparisons with `connect`
306+
307+
There are some differences between the selectors passed to `useSelector()` and a `mapState` function:
308+
309+
- The selector may return any value as a result, not just an object.
310+
- The selector normally _should_ return just a single value, and not an object. If you do return an object or an array, be sure to use a memoized selector to avoid unnecessary re-renders.
311+
- The selector function does _not_ receive an `ownProps` argument. However, props can be used through closure (see the examples above) or by using a curried selector.
312+
- You can use the `equalityFn` option to customize the comparison behavior
313+
243314
## `useDispatch()`
244315

245-
```js
246-
const dispatch = useDispatch()
316+
```ts
317+
import type { Dispatch } from 'redux'
318+
const dispatch: Dispatch = useDispatch()
247319
```
248320

249321
This hook returns a reference to the `dispatch` function from the Redux store. You may use it to dispatch actions as needed.
@@ -319,8 +391,9 @@ export const Todos = () => {
319391

320392
## `useStore()`
321393

322-
```js
323-
const store = useStore()
394+
```ts
395+
import type { Store } from 'redux'
396+
const store: Store = useStore()
324397
```
325398

326399
This hook returns a reference to the same Redux store that was passed in to the `<Provider>` component.
@@ -333,12 +406,19 @@ This hook should probably not be used frequently. Prefer `useSelector()` as your
333406
import React from 'react'
334407
import { useStore } from 'react-redux'
335408

336-
export const CounterComponent = ({ value }) => {
409+
export const ExampleComponent = ({ value }) => {
337410
const store = useStore()
338411

412+
const onClick = () => {
413+
// Not _recommended_, but safe
414+
// This avoids subscribing to the state via `useSelector`
415+
// Prefer moving this logic into a thunk instead
416+
const numTodos = store.getState().todos.length
417+
}
418+
339419
// EXAMPLE ONLY! Do not do this in a real app.
340420
// The component will not automatically update if the store state changes
341-
return <div>{store.getState()}</div>
421+
return <div>{store.getState().todos.length}</div>
342422
}
343423
```
344424

src/components/Context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createContext } from 'react'
22
import type { Action, AnyAction, Store } from 'redux'
33
import type { Subscription } from '../utils/Subscription'
4+
import { StabilityCheck } from '../hooks/useSelector'
45

56
export interface ReactReduxContextValue<
67
SS = any,
@@ -9,6 +10,7 @@ export interface ReactReduxContextValue<
910
store: Store<SS, A>
1011
subscription: Subscription
1112
getServerState?: () => SS
13+
stabilityCheck: StabilityCheck
1214
}
1315

1416
export const ReactReduxContext =

src/components/Provider.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ReactReduxContext, ReactReduxContextValue } from './Context'
33
import { createSubscription } from '../utils/Subscription'
44
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
55
import { Action, AnyAction, Store } from 'redux'
6+
import { StabilityCheck } from '../hooks/useSelector'
67

78
export interface ProviderProps<A extends Action = AnyAction, S = unknown> {
89
/**
@@ -21,6 +22,10 @@ export interface ProviderProps<A extends Action = AnyAction, S = unknown> {
2122
* Initial value doesn't matter, as it is overwritten with the internal state of Provider.
2223
*/
2324
context?: Context<ReactReduxContextValue<S, A>>
25+
26+
/** Global configuration for the `useSelector` stability check */
27+
stabilityCheck?: StabilityCheck
28+
2429
children: ReactNode
2530
}
2631

@@ -29,15 +34,17 @@ function Provider<A extends Action = AnyAction, S = unknown>({
2934
context,
3035
children,
3136
serverState,
37+
stabilityCheck = 'once',
3238
}: ProviderProps<A, S>) {
3339
const contextValue = useMemo(() => {
3440
const subscription = createSubscription(store)
3541
return {
3642
store,
3743
subscription,
3844
getServerState: serverState ? () => serverState : undefined,
45+
stabilityCheck,
3946
}
40-
}, [store, serverState])
47+
}, [store, serverState, stabilityCheck])
4148

4249
const previousState = useMemo(() => store.getState(), [store])
4350

src/hooks/useSelector.ts

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useDebugValue } from 'react'
1+
import { useCallback, useDebugValue, useRef } from 'react'
22

33
import {
44
createReduxContextHook,
@@ -9,6 +9,24 @@ import type { EqualityFn, NoInfer } from '../types'
99
import type { uSESWS } from '../utils/useSyncExternalStore'
1010
import { notInitialized } from '../utils/useSyncExternalStore'
1111

12+
export type StabilityCheck = 'never' | 'once' | 'always'
13+
14+
export interface UseSelectorOptions<Selected = unknown> {
15+
equalityFn?: EqualityFn<Selected>
16+
stabilityCheck?: StabilityCheck
17+
}
18+
19+
interface UseSelector {
20+
<TState = unknown, Selected = unknown>(
21+
selector: (state: TState) => Selected,
22+
equalityFn?: EqualityFn<Selected>
23+
): Selected
24+
<TState = unknown, Selected = unknown>(
25+
selector: (state: TState) => Selected,
26+
options?: UseSelectorOptions<Selected>
27+
): Selected
28+
}
29+
1230
let useSyncExternalStoreWithSelector = notInitialized as uSESWS
1331
export const initializeUseSelector = (fn: uSESWS) => {
1432
useSyncExternalStoreWithSelector = fn
@@ -22,21 +40,22 @@ const refEquality: EqualityFn<any> = (a, b) => a === b
2240
* @param {React.Context} [context=ReactReduxContext] Context passed to your `<Provider>`.
2341
* @returns {Function} A `useSelector` hook bound to the specified context.
2442
*/
25-
export function createSelectorHook(
26-
context = ReactReduxContext
27-
): <TState = unknown, Selected = unknown>(
28-
selector: (state: TState) => Selected,
29-
equalityFn?: EqualityFn<Selected>
30-
) => Selected {
43+
export function createSelectorHook(context = ReactReduxContext): UseSelector {
3144
const useReduxContext =
3245
context === ReactReduxContext
3346
? useDefaultReduxContext
3447
: createReduxContextHook(context)
3548

3649
return function useSelector<TState, Selected extends unknown>(
3750
selector: (state: TState) => Selected,
38-
equalityFn: EqualityFn<NoInfer<Selected>> = refEquality
51+
equalityFnOrOptions:
52+
| EqualityFn<NoInfer<Selected>>
53+
| UseSelectorOptions<NoInfer<Selected>> = {}
3954
): Selected {
55+
const { equalityFn = refEquality, stabilityCheck = undefined } =
56+
typeof equalityFnOrOptions === 'function'
57+
? { equalityFn: equalityFnOrOptions }
58+
: equalityFnOrOptions
4059
if (process.env.NODE_ENV !== 'production') {
4160
if (!selector) {
4261
throw new Error(`You must pass a selector to useSelector`)
@@ -51,13 +70,56 @@ export function createSelectorHook(
5170
}
5271
}
5372

54-
const { store, subscription, getServerState } = useReduxContext()!
73+
const {
74+
store,
75+
subscription,
76+
getServerState,
77+
stabilityCheck: globalStabilityCheck,
78+
} = useReduxContext()!
79+
80+
const firstRun = useRef(true)
81+
82+
const wrappedSelector = useCallback<typeof selector>(
83+
{
84+
[selector.name](state: TState) {
85+
const selected = selector(state)
86+
const finalStabilityCheck =
87+
// are we safe to use ?? here?
88+
typeof stabilityCheck === 'undefined'
89+
? globalStabilityCheck
90+
: stabilityCheck
91+
if (
92+
process.env.NODE_ENV !== 'production' &&
93+
(finalStabilityCheck === 'always' ||
94+
(finalStabilityCheck === 'once' && firstRun.current))
95+
) {
96+
const toCompare = selector(state)
97+
if (!equalityFn(selected, toCompare)) {
98+
console.warn(
99+
'Selector ' +
100+
(selector.name || 'unknown') +
101+
' returned a different result when called with the same parameters. This can lead to unnecessary rerenders.' +
102+
'\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',
103+
{
104+
state,
105+
selected,
106+
selected2: toCompare,
107+
}
108+
)
109+
}
110+
firstRun.current = false
111+
}
112+
return selected
113+
},
114+
}[selector.name],
115+
[selector, globalStabilityCheck, stabilityCheck]
116+
)
55117

56118
const selectedState = useSyncExternalStoreWithSelector(
57119
subscription.addNestedSub,
58120
store.getState,
59121
getServerState || store.getState,
60-
selector,
122+
wrappedSelector,
61123
equalityFn
62124
)
63125

src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type { NonReactStatics } from 'hoist-non-react-statics'
1111

1212
import type { ConnectProps } from './components/connect'
1313

14+
import { UseSelectorOptions } from './hooks/useSelector'
15+
1416
export type FixTypeLater = any
1517

1618
export type EqualityFn<T> = (a: T, b: T) => boolean
@@ -167,6 +169,10 @@ export interface TypedUseSelectorHook<TState> {
167169
selector: (state: TState) => TSelected,
168170
equalityFn?: EqualityFn<NoInfer<TSelected>>
169171
): TSelected
172+
<Selected = unknown>(
173+
selector: (state: TState) => Selected,
174+
options?: UseSelectorOptions<Selected>
175+
): Selected
170176
}
171177

172178
export type NoInfer<T> = [T][T extends any ? 0 : never]

0 commit comments

Comments
 (0)