Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename noopCheck to identityFunctionCheck #2091

Merged
merged 9 commits into from
Dec 3, 2023
34 changes: 25 additions & 9 deletions docs/api/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ From there, you may import any of the listed React Redux hooks APIs and use them
type RootState = ReturnType<typeof store.getState>
type SelectorFn = <Selected>(state: RootState) => Selected
type EqualityFn = (a: any, b: any) => boolean
export type CheckFrequency = 'never' | 'once' | 'always'
export type DevModeCheckFrequency = 'never' | 'once' | 'always'

interface UseSelectorOptions {
equalityFn?: EqualityFn
stabilityCheck?: CheckFrequency
noopCheck?: CheckFrequency
devModeChecks?: {
stabilityCheck?: DevModeCheckFrequency
identityFunctionCheck?: DevModeCheckFrequency
}
}

const result: Selected = useSelector(
Expand Down Expand Up @@ -296,14 +298,24 @@ By default, this will only happen when the selector is first called. You can con

```tsx title="Individual hook setting"
function Component() {
const count = useSelector(selectCount, { stabilityCheck: 'never' })
const count = useSelector(selectCount, {
devModeChecks: { stabilityCheck: 'never' },
})
// run once (default)
const user = useSelector(selectUser, { stabilityCheck: 'once' })
const user = useSelector(selectUser, {
devModeChecks: { stabilityCheck: 'once' },
})
// ...
}
```

#### No-op selector check
#### Identity Function (`state => state`) Check

:::danger Breaking Change!

This was previously referred to as `noopCheck`.

:::

In development, a check is conducted on the result returned by the selector. It warns in the console if the result is the same as the parameter passed in, i.e. the root state.

Expand All @@ -321,16 +333,20 @@ const user = useSelector((state) => state.auth.currentUser)
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.

```tsx title="Global setting via context"
<Provider store={store} noopCheck="always">
<Provider store={store} identityFunctionCheck="always">
{children}
</Provider>
```

```tsx title="Individual hook setting"
function Component() {
const count = useSelector(selectCount, { noopCheck: 'never' })
const count = useSelector(selectCount, {
devModeChecks: { identityFunctionCheck: 'never' },
})
// run once (default)
const user = useSelector(selectUser, { noopCheck: 'once' })
const user = useSelector(selectUser, {
devModeChecks: { identityFunctionCheck: 'once' },
})
// ...
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function mapStateToProps(state, ownProps) {
}

// Later, in your application, a parent component renders:
;<ConnectedTodo id={123} />
<ConnectedTodo id={123} />
// and your component receives props.id, props.todo, and props.visibilityFilter
```

Expand Down
8 changes: 3 additions & 5 deletions src/components/Context.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import * as React from 'react'
import type { Context } from 'react'
import * as React from 'react'
import type { Action, Store, UnknownAction } from 'redux'
import type { Subscription } from '../utils/Subscription'
import type { CheckFrequency } from '../hooks/useSelector'
import type { ProviderProps } from './Provider'

export interface ReactReduxContextValue<
SS = any,
A extends Action<string> = UnknownAction
> {
> extends Pick<ProviderProps, 'stabilityCheck' | 'identityFunctionCheck'> {
store: Store<SS, A>
subscription: Subscription
getServerState?: () => SS
stabilityCheck: CheckFrequency
noopCheck: CheckFrequency
}

const ContextKey = Symbol.for(`react-redux-context`)
Expand Down
38 changes: 27 additions & 11 deletions src/components/Provider.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Context, ReactNode } from 'react'
import * as React from 'react'
import type { ReactReduxContextValue } from './Context'
import { ReactReduxContext } from './Context'
import type { Action, Store, UnknownAction } from 'redux'
import type { DevModeCheckFrequency } from '../hooks/useSelector'
import { createSubscription } from '../utils/Subscription'
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
import type { Action, Store, UnknownAction } from 'redux'
import type { CheckFrequency } from '../hooks/useSelector'
import type { ReactReduxContextValue } from './Context'
import { ReactReduxContext } from './Context'

export interface ProviderProps<
A extends Action<string> = UnknownAction,
Expand All @@ -29,11 +29,27 @@ export interface ProviderProps<
*/
context?: Context<ReactReduxContextValue<S, A> | null>

/** Global configuration for the `useSelector` stability check */
stabilityCheck?: CheckFrequency
/**
* Determines the frequency of stability checks for all selectors.
* This setting overrides the global configuration for
* the `useSelector` stability check, allowing you to specify how often
* these checks should occur in development mode.
*
* @since 8.1.0
*/
stabilityCheck?: DevModeCheckFrequency

/** Global configuration for the `useSelector` no-op check */
noopCheck?: CheckFrequency
/**
* Determines the frequency of identity function checks for all selectors.
* This setting overrides the global configuration for
* the `useSelector` identity function check, allowing you to specify how often
* these checks should occur in development mode.
*
* **Note**: Previously referred to as `noopCheck`.
*
* @since 9.0.0
*/
identityFunctionCheck?: DevModeCheckFrequency

children: ReactNode
}
Expand All @@ -44,7 +60,7 @@ function Provider<A extends Action<string> = UnknownAction, S = unknown>({
children,
serverState,
stabilityCheck = 'once',
noopCheck = 'once',
identityFunctionCheck = 'once',
}: ProviderProps<A, S>) {
const contextValue = React.useMemo(() => {
const subscription = createSubscription(store)
Expand All @@ -53,9 +69,9 @@ function Provider<A extends Action<string> = UnknownAction, S = unknown>({
subscription,
getServerState: serverState ? () => serverState : undefined,
stabilityCheck,
noopCheck,
identityFunctionCheck,
}
}, [store, serverState, stabilityCheck, noopCheck])
}, [store, serverState, stabilityCheck, identityFunctionCheck])

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

Expand Down
96 changes: 71 additions & 25 deletions src/hooks/useSelector.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,68 @@
import * as React from 'react'

import {
createReduxContextHook,
useReduxContext as useDefaultReduxContext,
} from './useReduxContext'
import type { ReactReduxContextValue } from '../components/Context'
import { ReactReduxContext } from '../components/Context'
import type { EqualityFn, NoInfer } from '../types'
import type { uSESWS } from '../utils/useSyncExternalStore'
import { notInitialized } from '../utils/useSyncExternalStore'
import {
createReduxContextHook,
useReduxContext as useDefaultReduxContext,
} from './useReduxContext'

/**
* The frequency of development mode checks.
*
* @since 8.1.0
* @internal
*/
export type DevModeCheckFrequency = 'never' | 'once' | 'always'

/**
* Represents the configuration for development mode checks.
*
* @since 9.0.0
* @internal
*/
export interface DevModeChecks {
/**
* Overrides the global stability check for the selector.
* - `once` - Run only the first time the selector is called.
* - `always` - Run every time the selector is called.
* - `never` - Never run the stability check.
*
* @default 'once'
*
* @since 8.1.0
*/
stabilityCheck: DevModeCheckFrequency

export type CheckFrequency = 'never' | 'once' | 'always'
/**
* Overrides the global identity function check for the selector.
* - `once` - Run only the first time the selector is called.
* - `always` - Run every time the selector is called.
* - `never` - Never run the identity function check.
*
* **Note**: Previously referred to as `noopCheck`.
*
* @default 'once'
*
* @since 9.0.0
*/
identityFunctionCheck: DevModeCheckFrequency
}

export interface UseSelectorOptions<Selected = unknown> {
equalityFn?: EqualityFn<Selected>
stabilityCheck?: CheckFrequency
noopCheck?: CheckFrequency

/**
* `useSelector` performs additional checks in development mode to help
* identify and warn about potential issues in selector behavior. This
* option allows you to customize the behavior of these checks per selector.
*
* @since 9.0.0
*/
devModeChecks?: Partial<DevModeChecks>
}

export interface UseSelector {
Expand Down Expand Up @@ -59,13 +106,10 @@ export function createSelectorHook(
| EqualityFn<NoInfer<Selected>>
| UseSelectorOptions<NoInfer<Selected>> = {}
): Selected {
const {
equalityFn = refEquality,
stabilityCheck = undefined,
noopCheck = undefined,
} = typeof equalityFnOrOptions === 'function'
? { equalityFn: equalityFnOrOptions }
: equalityFnOrOptions
const { equalityFn = refEquality, devModeChecks = {} } =
typeof equalityFnOrOptions === 'function'
? { equalityFn: equalityFnOrOptions }
: equalityFnOrOptions
if (process.env.NODE_ENV !== 'production') {
if (!selector) {
throw new Error(`You must pass a selector to useSelector`)
Expand All @@ -84,8 +128,8 @@ export function createSelectorHook(
store,
subscription,
getServerState,
stabilityCheck: globalStabilityCheck,
noopCheck: globalNoopCheck,
stabilityCheck,
identityFunctionCheck,
} = useReduxContext()

const firstRun = React.useRef(true)
Expand All @@ -95,10 +139,14 @@ export function createSelectorHook(
[selector.name](state: TState) {
const selected = selector(state)
if (process.env.NODE_ENV !== 'production') {
const finalStabilityCheck =
typeof stabilityCheck === 'undefined'
? globalStabilityCheck
: stabilityCheck
const {
identityFunctionCheck: finalIdentityFunctionCheck,
stabilityCheck: finalStabilityCheck,
} = {
stabilityCheck,
identityFunctionCheck,
...devModeChecks,
}
if (
finalStabilityCheck === 'always' ||
(finalStabilityCheck === 'once' && firstRun.current)
Expand All @@ -125,11 +173,9 @@ export function createSelectorHook(
)
}
}
const finalNoopCheck =
typeof noopCheck === 'undefined' ? globalNoopCheck : noopCheck
if (
finalNoopCheck === 'always' ||
(finalNoopCheck === 'once' && firstRun.current)
finalIdentityFunctionCheck === 'always' ||
(finalIdentityFunctionCheck === 'once' && firstRun.current)
) {
// @ts-ignore
if (selected === state) {
Expand All @@ -153,7 +199,7 @@ export function createSelectorHook(
return selected
},
}[selector.name],
[selector, globalStabilityCheck, stabilityCheck]
[selector, stabilityCheck, devModeChecks.stabilityCheck]
)

const selectedState = useSyncExternalStoreWithSelector(
Expand Down
Loading
Loading