Skip to content

Commit

Permalink
Merge pull request #2091 from aryaemami59/identity-function
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson authored Dec 3, 2023
2 parents b748c5c + 07f3060 commit f3b52f5
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 99 deletions.
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

0 comments on commit f3b52f5

Please sign in to comment.