diff --git a/src/components/Context.ts b/src/components/Context.ts index 5c49d5a0e..a0b5cab09 100644 --- a/src/components/Context.ts +++ b/src/components/Context.ts @@ -2,7 +2,7 @@ import { createContext } from 'react' import type { Context } from 'react' import type { Action, AnyAction, Store } from 'redux' import type { Subscription } from '../utils/Subscription' -import { StabilityCheck } from '../hooks/useSelector' +import { CheckFrequency } from '../hooks/useSelector' export interface ReactReduxContextValue< SS = any, @@ -11,7 +11,8 @@ export interface ReactReduxContextValue< store: Store subscription: Subscription getServerState?: () => SS - stabilityCheck: StabilityCheck + stabilityCheck: CheckFrequency + noopCheck: CheckFrequency } let realContext: Context | null = null diff --git a/src/components/Provider.tsx b/src/components/Provider.tsx index 55cb71b80..a11eaf55d 100644 --- a/src/components/Provider.tsx +++ b/src/components/Provider.tsx @@ -3,7 +3,7 @@ import { ReactReduxContext, ReactReduxContextValue } from './Context' import { createSubscription } from '../utils/Subscription' import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect' import { Action, AnyAction, Store } from 'redux' -import { StabilityCheck } from '../hooks/useSelector' +import { CheckFrequency } from '../hooks/useSelector' export interface ProviderProps { /** @@ -24,7 +24,10 @@ export interface ProviderProps { context?: Context> /** Global configuration for the `useSelector` stability check */ - stabilityCheck?: StabilityCheck + stabilityCheck?: CheckFrequency + + /** Global configuration for the `useSelector` no-op check */ + noopCheck?: CheckFrequency children: ReactNode } @@ -35,6 +38,7 @@ function Provider({ children, serverState, stabilityCheck = 'once', + noopCheck = 'once', }: ProviderProps) { const contextValue = useMemo(() => { const subscription = createSubscription(store) @@ -43,8 +47,9 @@ function Provider({ subscription, getServerState: serverState ? () => serverState : undefined, stabilityCheck, + noopCheck, } - }, [store, serverState, stabilityCheck]) + }, [store, serverState, stabilityCheck, noopCheck]) const previousState = useMemo(() => store.getState(), [store]) diff --git a/src/hooks/useSelector.ts b/src/hooks/useSelector.ts index f20ff17fb..0415e908b 100644 --- a/src/hooks/useSelector.ts +++ b/src/hooks/useSelector.ts @@ -9,11 +9,12 @@ import type { EqualityFn, NoInfer } from '../types' import type { uSESWS } from '../utils/useSyncExternalStore' import { notInitialized } from '../utils/useSyncExternalStore' -export type StabilityCheck = 'never' | 'once' | 'always' +export type CheckFrequency = 'never' | 'once' | 'always' export interface UseSelectorOptions { equalityFn?: EqualityFn - stabilityCheck?: StabilityCheck + stabilityCheck?: CheckFrequency + noopCheck?: CheckFrequency } interface UseSelector { @@ -52,10 +53,13 @@ export function createSelectorHook(context = ReactReduxContext): UseSelector { | EqualityFn> | UseSelectorOptions> = {} ): Selected { - const { equalityFn = refEquality, stabilityCheck = undefined } = - typeof equalityFnOrOptions === 'function' - ? { equalityFn: equalityFnOrOptions } - : equalityFnOrOptions + const { + equalityFn = refEquality, + stabilityCheck = undefined, + noopCheck = undefined, + } = typeof equalityFnOrOptions === 'function' + ? { equalityFn: equalityFnOrOptions } + : equalityFnOrOptions if (process.env.NODE_ENV !== 'production') { if (!selector) { throw new Error(`You must pass a selector to useSelector`) @@ -75,6 +79,7 @@ export function createSelectorHook(context = ReactReduxContext): UseSelector { subscription, getServerState, stabilityCheck: globalStabilityCheck, + noopCheck: globalNoopCheck, } = useReduxContext()! const firstRun = useRef(true) @@ -83,31 +88,49 @@ export function createSelectorHook(context = ReactReduxContext): UseSelector { { [selector.name](state: TState) { const selected = selector(state) - const finalStabilityCheck = - // are we safe to use ?? here? - typeof stabilityCheck === 'undefined' - ? globalStabilityCheck - : stabilityCheck - if ( - process.env.NODE_ENV !== 'production' && - (finalStabilityCheck === 'always' || - (finalStabilityCheck === 'once' && firstRun.current)) - ) { - const toCompare = selector(state) - if (!equalityFn(selected, toCompare)) { - console.warn( - 'Selector ' + - (selector.name || 'unknown') + - ' returned a different result when called with the same parameters. This can lead to unnecessary rerenders.' + - '\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', - { - state, - selected, - selected2: toCompare, - } - ) + if (process.env.NODE_ENV !== 'production') { + const finalStabilityCheck = + // are we safe to use ?? here? + typeof stabilityCheck === 'undefined' + ? globalStabilityCheck + : stabilityCheck + if ( + finalStabilityCheck === 'always' || + (finalStabilityCheck === 'once' && firstRun.current) + ) { + const toCompare = selector(state) + if (!equalityFn(selected, toCompare)) { + console.warn( + 'Selector ' + + (selector.name || 'unknown') + + ' returned a different result when called with the same parameters. This can lead to unnecessary rerenders.' + + '\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', + { + state, + selected, + selected2: toCompare, + } + ) + } + } + const finalNoopCheck = + // are we safe to use ?? here? + typeof noopCheck === 'undefined' ? globalNoopCheck : noopCheck + if ( + finalNoopCheck === 'always' || + (finalNoopCheck === 'once' && firstRun.current) + ) { + // @ts-ignore + if (selected === state) { + console.warn( + 'Selector ' + + (selector.name || 'unknown') + + ' returned the root state when called. This can lead to unnecessary rerenders.' + + '\nSelectors that return the entire state are almost certainly a mistake, as they will cause a rerender whenever *anything* in state changes.' + ) + } } - firstRun.current = false + if (firstRun.current) firstRun.current = false } return selected },