diff --git a/docs/api/hooks.md b/docs/api/hooks.md index 90ba18fdd..6ed03157c 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -48,12 +48,14 @@ From there, you may import any of the listed React Redux hooks APIs and use them type RootState = ReturnType type SelectorFn = (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( @@ -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. @@ -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" - + {children} ``` ```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' }, + }) // ... } ``` diff --git a/docs/using-react-redux/connect-extracting-data-with-mapStateToProps.md b/docs/using-react-redux/connect-extracting-data-with-mapStateToProps.md index 6f3ff30cb..ea3fd4deb 100644 --- a/docs/using-react-redux/connect-extracting-data-with-mapStateToProps.md +++ b/docs/using-react-redux/connect-extracting-data-with-mapStateToProps.md @@ -69,7 +69,7 @@ function mapStateToProps(state, ownProps) { } // Later, in your application, a parent component renders: -; + // and your component receives props.id, props.todo, and props.visibilityFilter ``` diff --git a/src/components/Context.ts b/src/components/Context.ts index 993d6613a..006942227 100644 --- a/src/components/Context.ts +++ b/src/components/Context.ts @@ -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 = UnknownAction -> { +> extends Pick { store: Store subscription: Subscription getServerState?: () => SS - stabilityCheck: CheckFrequency - noopCheck: CheckFrequency } const ContextKey = Symbol.for(`react-redux-context`) diff --git a/src/components/Provider.tsx b/src/components/Provider.tsx index 7ddfc7e89..e93a94fad 100644 --- a/src/components/Provider.tsx +++ b/src/components/Provider.tsx @@ -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 = UnknownAction, @@ -29,11 +29,27 @@ export interface ProviderProps< */ context?: Context | 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 } @@ -44,7 +60,7 @@ function Provider = UnknownAction, S = unknown>({ children, serverState, stabilityCheck = 'once', - noopCheck = 'once', + identityFunctionCheck = 'once', }: ProviderProps) { const contextValue = React.useMemo(() => { const subscription = createSubscription(store) @@ -53,9 +69,9 @@ function Provider = 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]) diff --git a/src/hooks/useSelector.ts b/src/hooks/useSelector.ts index e9a34838f..9f1d7b697 100644 --- a/src/hooks/useSelector.ts +++ b/src/hooks/useSelector.ts @@ -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 { equalityFn?: EqualityFn - 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 } export interface UseSelector { @@ -59,13 +106,10 @@ export function createSelectorHook( | EqualityFn> | UseSelectorOptions> = {} ): 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`) @@ -84,8 +128,8 @@ export function createSelectorHook( store, subscription, getServerState, - stabilityCheck: globalStabilityCheck, - noopCheck: globalNoopCheck, + stabilityCheck, + identityFunctionCheck, } = useReduxContext() const firstRun = React.useRef(true) @@ -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) @@ -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) { @@ -153,7 +199,7 @@ export function createSelectorHook( return selected }, }[selector.name], - [selector, globalStabilityCheck, stabilityCheck] + [selector, stabilityCheck, devModeChecks.stabilityCheck] ) const selectedState = useSyncExternalStoreWithSelector( diff --git a/test/hooks/useSelector.spec.tsx b/test/hooks/useSelector.spec.tsx index debc1c426..9108d18ff 100644 --- a/test/hooks/useSelector.spec.tsx +++ b/test/hooks/useSelector.spec.tsx @@ -1,46 +1,46 @@ /*eslint-disable react/prop-types*/ +import * as rtl from '@testing-library/react' +import type { DispatchWithoutAction, FunctionComponent, ReactNode } from 'react' import React, { + Suspense, useCallback, - useReducer, - useLayoutEffect, - useState, useContext, - Suspense, useEffect, + useLayoutEffect, + useReducer, + useState, } from 'react' +import type { Action, AnyAction, Store } from 'redux' import { createStore } from 'redux' -import * as rtl from '@testing-library/react' +import type { UseSelectorOptions } from '../../src/hooks/useSelector' +import type { + ProviderProps, + ReactReduxContextValue, + Subscription, + TypedUseSelectorHook, +} from '../../src/index' import { Provider, - useSelector, - useDispatch, - shallowEqual, + ReactReduxContext, connect, createSelectorHook, - ReactReduxContext, -} from '../../src/index' -import type { - TypedUseSelectorHook, - ReactReduxContextValue, - ProviderProps, - Subscription, + shallowEqual, + useDispatch, + useSelector, } from '../../src/index' -import type { FunctionComponent, DispatchWithoutAction, ReactNode } from 'react' -import type { Store, AnyAction, Action } from 'redux' -import type { UseSelectorOptions } from '../../src/hooks/useSelector' // disable checks by default function ProviderMock = AnyAction, S = unknown>({ stabilityCheck = 'never', - noopCheck = 'never', + identityFunctionCheck = 'never', ...props }: ProviderProps) { return ( ) } @@ -984,7 +984,7 @@ describe('React', () => { ) @@ -1014,7 +1014,7 @@ describe('React', () => { ) @@ -1028,10 +1028,10 @@ describe('React', () => { expect(selector).toHaveBeenCalledTimes(4) }) }) - describe('no-op selector check', () => { + describe('identity function check', () => { it('warns for selectors that return the entire root state', () => { rtl.render( - + state.count} /> ) @@ -1041,7 +1041,7 @@ describe('React', () => { rtl.cleanup() rtl.render( - + state} /> ) diff --git a/test/typetests/hooks.tsx b/test/typetests/hooks.tsx index c35cc0be2..a553ed29e 100644 --- a/test/typetests/hooks.tsx +++ b/test/typetests/hooks.tsx @@ -1,42 +1,27 @@ /* eslint-disable @typescript-eslint/no-unused-vars, no-inner-declarations */ -import * as React from 'react' -import * as ReactDOM from 'react-dom' -import type { Store, Dispatch, AnyAction } from '@reduxjs/toolkit' +import type { AnyAction, Dispatch, Store } from '@reduxjs/toolkit' import { configureStore } from '@reduxjs/toolkit' +import * as React from 'react' import type { ReactReduxContextValue, Selector, TypedUseSelectorHook, } from '../../src/index' import { - connect, - ConnectedProps, - Provider, - DispatchProp, - MapStateToProps, - ReactReduxContext, + createDispatchHook, + createSelectorHook, + createStoreHook, shallowEqual, - MapDispatchToProps, useDispatch, useSelector, useStore, - createDispatchHook, - createSelectorHook, - createStoreHook, } from '../../src/index' import type { AppDispatch, RootState } from './counterApp' -import { - CounterState, - counterSlice, - increment, - incrementAsync, - AppThunk, - fetchCount, -} from './counterApp' +import { incrementAsync } from './counterApp' -import { expectType, expectExactType } from '../typeTestHelpers' +import { expectExactType, expectType } from '../typeTestHelpers' function preTypedHooksSetup() { // Standard hooks setup @@ -172,7 +157,7 @@ function testUseSelector() { const correctlyInferred: State = useSelector(selector, shallowEqual) const correctlyInferred2: State = useSelector(selector, { equalityFn: shallowEqual, - stabilityCheck: 'never', + devModeChecks: { stabilityCheck: 'never' }, }) // @ts-expect-error const inferredTypeIsNotString: string = useSelector(selector, shallowEqual)