Skip to content

Commit b0b4263

Browse files
committed
Add devModeChecks to useSelector
1 parent e8ed4b6 commit b0b4263

File tree

5 files changed

+102
-69
lines changed

5 files changed

+102
-69
lines changed

src/components/Context.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import type { Context } from 'react'
22
import * as React from 'react'
33
import type { Action, Store, UnknownAction } from 'redux'
4-
import type { DevModeCheckFrequency } from '../hooks/useSelector'
54
import type { Subscription } from '../utils/Subscription'
5+
import type { ProviderProps } from './Provider'
66

77
export interface ReactReduxContextValue<
88
SS = any,
99
A extends Action<string> = UnknownAction
10-
> {
10+
> extends Pick<ProviderProps, 'stabilityCheck' | 'identityFunctionCheck'> {
1111
store: Store<SS, A>
1212
subscription: Subscription
1313
getServerState?: () => SS
14-
stabilityCheck: DevModeCheckFrequency
15-
identityFunctionCheck: DevModeCheckFrequency
1614
}
1715

1816
const ContextKey = Symbol.for(`react-redux-context`)

src/components/Provider.tsx

+16-2
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,24 @@ export interface ProviderProps<
2929
*/
3030
context?: Context<ReactReduxContextValue<S, A> | null>
3131

32-
/** Global configuration for the `useSelector` stability check */
32+
/**
33+
* Determines the frequency of stability checks for all selectors.
34+
* This setting overrides the global configuration for
35+
* the `useSelector` stability check, allowing you to specify how often
36+
* these checks should occur in development mode.
37+
*
38+
* @since 8.1.0
39+
*/
3340
stabilityCheck?: DevModeCheckFrequency
3441

35-
/** Global configuration for the `useSelector` identity function check */
42+
/**
43+
* Determines the frequency of identity function checks for all selectors.
44+
* This setting overrides the global configuration for
45+
* the `useSelector` identity function check, allowing you to specify how often
46+
* these checks should occur in development mode.
47+
*
48+
* @since 9.0.0
49+
*/
3650
identityFunctionCheck?: DevModeCheckFrequency
3751

3852
children: ReactNode

src/hooks/useSelector.ts

+56-20
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,49 @@ import {
1818
*/
1919
export type DevModeCheckFrequency = 'never' | 'once' | 'always'
2020

21+
/**
22+
* Represents the configuration for development mode checks.
23+
*
24+
* @since 9.0.0
25+
* @internal
26+
*/
27+
export interface DevModeChecks {
28+
/**
29+
* Overrides the global stability check for the selector.
30+
* - `once` - Run only the first time the selector is called.
31+
* - `always` - Run every time the selector is called.
32+
* - `never` - Never run the stability check.
33+
*
34+
* @default 'once'
35+
*
36+
* @since 8.1.0
37+
*/
38+
stabilityCheck: DevModeCheckFrequency
39+
40+
/**
41+
* Overrides the global identity function check for the selector.
42+
* - `once` - Run only the first time the selector is called.
43+
* - `always` - Run every time the selector is called.
44+
* - `never` - Never run the identity function check.
45+
*
46+
* @default 'once'
47+
*
48+
* @since 9.0.0
49+
*/
50+
identityFunctionCheck: DevModeCheckFrequency
51+
}
52+
2153
export interface UseSelectorOptions<Selected = unknown> {
2254
equalityFn?: EqualityFn<Selected>
23-
stabilityCheck?: DevModeCheckFrequency
24-
identityFunctionCheck?: DevModeCheckFrequency
55+
56+
/**
57+
* `useSelector` performs additional checks in development mode to help
58+
* identify and warn about potential issues in selector behavior. This
59+
* option allows you to customize the behavior of these checks per selector.
60+
*
61+
* @since 9.0.0
62+
*/
63+
devModeChecks?: Partial<DevModeChecks>
2564
}
2665

2766
export interface UseSelector {
@@ -65,13 +104,10 @@ export function createSelectorHook(
65104
| EqualityFn<NoInfer<Selected>>
66105
| UseSelectorOptions<NoInfer<Selected>> = {}
67106
): Selected {
68-
const {
69-
equalityFn = refEquality,
70-
stabilityCheck = undefined,
71-
identityFunctionCheck = undefined,
72-
} = typeof equalityFnOrOptions === 'function'
73-
? { equalityFn: equalityFnOrOptions }
74-
: equalityFnOrOptions
107+
const { equalityFn = refEquality, devModeChecks = {} } =
108+
typeof equalityFnOrOptions === 'function'
109+
? { equalityFn: equalityFnOrOptions }
110+
: equalityFnOrOptions
75111
if (process.env.NODE_ENV !== 'production') {
76112
if (!selector) {
77113
throw new Error(`You must pass a selector to useSelector`)
@@ -90,8 +126,8 @@ export function createSelectorHook(
90126
store,
91127
subscription,
92128
getServerState,
93-
stabilityCheck: globalStabilityCheck,
94-
identityFunctionCheck: globalIdentityFunctionCheck,
129+
stabilityCheck,
130+
identityFunctionCheck,
95131
} = useReduxContext()
96132

97133
const firstRun = React.useRef(true)
@@ -101,10 +137,14 @@ export function createSelectorHook(
101137
[selector.name](state: TState) {
102138
const selected = selector(state)
103139
if (process.env.NODE_ENV !== 'production') {
104-
const finalStabilityCheck =
105-
typeof stabilityCheck === 'undefined'
106-
? globalStabilityCheck
107-
: stabilityCheck
140+
const {
141+
identityFunctionCheck: finalIdentityFunctionCheck,
142+
stabilityCheck: finalStabilityCheck,
143+
} = {
144+
stabilityCheck,
145+
identityFunctionCheck,
146+
...devModeChecks,
147+
}
108148
if (
109149
finalStabilityCheck === 'always' ||
110150
(finalStabilityCheck === 'once' && firstRun.current)
@@ -131,10 +171,6 @@ export function createSelectorHook(
131171
)
132172
}
133173
}
134-
const finalIdentityFunctionCheck =
135-
typeof identityFunctionCheck === 'undefined'
136-
? globalIdentityFunctionCheck
137-
: identityFunctionCheck
138174
if (
139175
finalIdentityFunctionCheck === 'always' ||
140176
(finalIdentityFunctionCheck === 'once' && firstRun.current)
@@ -161,7 +197,7 @@ export function createSelectorHook(
161197
return selected
162198
},
163199
}[selector.name],
164-
[selector, globalStabilityCheck, stabilityCheck]
200+
[selector, stabilityCheck, devModeChecks.stabilityCheck]
165201
)
166202

167203
const selectedState = useSyncExternalStoreWithSelector(

test/hooks/useSelector.spec.tsx

+20-20
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,34 @@
11
/*eslint-disable react/prop-types*/
22

3+
import * as rtl from '@testing-library/react'
4+
import type { DispatchWithoutAction, FunctionComponent, ReactNode } from 'react'
35
import React, {
6+
Suspense,
47
useCallback,
5-
useReducer,
6-
useLayoutEffect,
7-
useState,
88
useContext,
9-
Suspense,
109
useEffect,
10+
useLayoutEffect,
11+
useReducer,
12+
useState,
1113
} from 'react'
14+
import type { Action, AnyAction, Store } from 'redux'
1215
import { createStore } from 'redux'
13-
import * as rtl from '@testing-library/react'
16+
import type { UseSelectorOptions } from '../../src/hooks/useSelector'
17+
import type {
18+
ProviderProps,
19+
ReactReduxContextValue,
20+
Subscription,
21+
TypedUseSelectorHook,
22+
} from '../../src/index'
1423
import {
1524
Provider,
16-
useSelector,
17-
useDispatch,
18-
shallowEqual,
25+
ReactReduxContext,
1926
connect,
2027
createSelectorHook,
21-
ReactReduxContext,
22-
} from '../../src/index'
23-
import type {
24-
TypedUseSelectorHook,
25-
ReactReduxContextValue,
26-
ProviderProps,
27-
Subscription,
28+
shallowEqual,
29+
useDispatch,
30+
useSelector,
2831
} from '../../src/index'
29-
import type { FunctionComponent, DispatchWithoutAction, ReactNode } from 'react'
30-
import type { Store, AnyAction, Action } from 'redux'
31-
import type { UseSelectorOptions } from '../../src/hooks/useSelector'
3232

3333
// disable checks by default
3434
function ProviderMock<A extends Action<any> = AnyAction, S = unknown>({
@@ -984,7 +984,7 @@ describe('React', () => {
984984
<ProviderMock stabilityCheck="once" store={normalStore}>
985985
<RenderSelector
986986
selector={selector}
987-
options={{ stabilityCheck: 'never' }}
987+
options={{ devModeChecks: { stabilityCheck: 'never' } }}
988988
/>
989989
</ProviderMock>
990990
)
@@ -1014,7 +1014,7 @@ describe('React', () => {
10141014
<ProviderMock stabilityCheck="once" store={normalStore}>
10151015
<RenderSelector
10161016
selector={selector}
1017-
options={{ stabilityCheck: 'always' }}
1017+
options={{ devModeChecks: { stabilityCheck: 'always' } }}
10181018
/>
10191019
</ProviderMock>
10201020
)

test/typetests/hooks.tsx

+8-23
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,27 @@
11
/* eslint-disable @typescript-eslint/no-unused-vars, no-inner-declarations */
22

3-
import * as React from 'react'
4-
import * as ReactDOM from 'react-dom'
5-
import type { Store, Dispatch, AnyAction } from '@reduxjs/toolkit'
3+
import type { AnyAction, Dispatch, Store } from '@reduxjs/toolkit'
64
import { configureStore } from '@reduxjs/toolkit'
5+
import * as React from 'react'
76
import type {
87
ReactReduxContextValue,
98
Selector,
109
TypedUseSelectorHook,
1110
} from '../../src/index'
1211
import {
13-
connect,
14-
ConnectedProps,
15-
Provider,
16-
DispatchProp,
17-
MapStateToProps,
18-
ReactReduxContext,
12+
createDispatchHook,
13+
createSelectorHook,
14+
createStoreHook,
1915
shallowEqual,
20-
MapDispatchToProps,
2116
useDispatch,
2217
useSelector,
2318
useStore,
24-
createDispatchHook,
25-
createSelectorHook,
26-
createStoreHook,
2719
} from '../../src/index'
2820

2921
import type { AppDispatch, RootState } from './counterApp'
30-
import {
31-
CounterState,
32-
counterSlice,
33-
increment,
34-
incrementAsync,
35-
AppThunk,
36-
fetchCount,
37-
} from './counterApp'
22+
import { incrementAsync } from './counterApp'
3823

39-
import { expectType, expectExactType } from '../typeTestHelpers'
24+
import { expectExactType, expectType } from '../typeTestHelpers'
4025

4126
function preTypedHooksSetup() {
4227
// Standard hooks setup
@@ -172,7 +157,7 @@ function testUseSelector() {
172157
const correctlyInferred: State = useSelector(selector, shallowEqual)
173158
const correctlyInferred2: State = useSelector(selector, {
174159
equalityFn: shallowEqual,
175-
stabilityCheck: 'never',
160+
devModeChecks: { stabilityCheck: 'never' },
176161
})
177162
// @ts-expect-error
178163
const inferredTypeIsNotString: string = useSelector(selector, shallowEqual)

0 commit comments

Comments
 (0)