UNPKG

react-redux

Version:

Official React bindings for Redux

287 lines (266 loc) 9.76 kB
//import * as React from 'react' import { React } from '../utils/react' import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector.js' import type { ReactReduxContextValue } from '../components/Context' import { ReactReduxContext } from '../components/Context' import type { EqualityFn, NoInfer } from '../types' 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 /** * 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> /** * `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> } /** * Represents a custom hook that allows you to extract data from the * Redux store state, using a selector function. The selector function * takes the current state as an argument and returns a part of the state * or some derived data. The hook also supports an optional equality * function or options object to customize its behavior. * * @template StateType - The specific type of state this hook operates on. * * @public */ export interface UseSelector<StateType = unknown> { /** * A function that takes a selector function as its first argument. * The selector function is responsible for selecting a part of * the Redux store's state or computing derived data. * * @param selector - A function that receives the current state and returns a part of the state or some derived data. * @param equalityFnOrOptions - An optional equality function or options object for customizing the behavior of the selector. * @returns The selected part of the state or derived data. * * @template TState - The specific type of state this hook operates on. * @template Selected - The type of the value that the selector function will return. */ <TState extends StateType = StateType, Selected = unknown>( selector: (state: TState) => Selected, equalityFnOrOptions?: EqualityFn<Selected> | UseSelectorOptions<Selected>, ): Selected /** * Creates a "pre-typed" version of {@linkcode useSelector useSelector} * where the `state` type is predefined. * * This allows you to set the `state` type once, eliminating the need to * specify it with every {@linkcode useSelector useSelector} call. * * @returns A pre-typed `useSelector` with the state type already defined. * * @example * ```ts * export const useAppSelector = useSelector.withTypes<RootState>() * ``` * * @template OverrideStateType - The specific type of state this hook operates on. * * @since 9.1.0 */ withTypes: < OverrideStateType extends StateType, >() => UseSelector<OverrideStateType> } const refEquality: EqualityFn<any> = (a, b) => a === b /** * Hook factory, which creates a `useSelector` hook bound to a given context. * * @param {React.Context} [context=ReactReduxContext] Context passed to your `<Provider>`. * @returns {Function} A `useSelector` hook bound to the specified context. */ export function createSelectorHook( context: React.Context<ReactReduxContextValue< any, any > | null> = ReactReduxContext, ): UseSelector { const useReduxContext = context === ReactReduxContext ? useDefaultReduxContext : createReduxContextHook(context) const useSelector = <TState, Selected>( selector: (state: TState) => Selected, equalityFnOrOptions: | EqualityFn<NoInfer<Selected>> | UseSelectorOptions<NoInfer<Selected>> = {}, ): Selected => { const { equalityFn = refEquality } = typeof equalityFnOrOptions === 'function' ? { equalityFn: equalityFnOrOptions } : equalityFnOrOptions if (process.env.NODE_ENV !== 'production') { if (!selector) { throw new Error(`You must pass a selector to useSelector`) } if (typeof selector !== 'function') { throw new Error(`You must pass a function as a selector to useSelector`) } if (typeof equalityFn !== 'function') { throw new Error( `You must pass a function as an equality function to useSelector`, ) } } const reduxContext = useReduxContext() const { store, subscription, getServerState } = reduxContext const firstRun = React.useRef(true) const wrappedSelector = React.useCallback<typeof selector>( { [selector.name](state: TState) { const selected = selector(state) if (process.env.NODE_ENV !== 'production') { const { devModeChecks = {} } = typeof equalityFnOrOptions === 'function' ? {} : equalityFnOrOptions const { identityFunctionCheck, stabilityCheck } = reduxContext const { identityFunctionCheck: finalIdentityFunctionCheck, stabilityCheck: finalStabilityCheck, } = { stabilityCheck, identityFunctionCheck, ...devModeChecks, } if ( finalStabilityCheck === 'always' || (finalStabilityCheck === 'once' && firstRun.current) ) { const toCompare = selector(state) if (!equalityFn(selected, toCompare)) { let stack: string | undefined = undefined try { throw new Error() } catch (e) { // eslint-disable-next-line no-extra-semi ;({ stack } = e as Error) } 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, stack, }, ) } } if ( finalIdentityFunctionCheck === 'always' || (finalIdentityFunctionCheck === 'once' && firstRun.current) ) { // @ts-ignore if (selected === state) { let stack: string | undefined = undefined try { throw new Error() } catch (e) { // eslint-disable-next-line no-extra-semi ;({ stack } = e as Error) } 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.', { stack }, ) } } if (firstRun.current) firstRun.current = false } return selected }, }[selector.name], [selector], ) const selectedState = useSyncExternalStoreWithSelector( subscription.addNestedSub, store.getState, getServerState || store.getState, wrappedSelector, equalityFn, ) React.useDebugValue(selectedState) return selectedState } Object.assign(useSelector, { withTypes: () => useSelector, }) return useSelector as UseSelector } /** * A hook to access the redux store's state. This hook takes a selector function * as an argument. The selector is called with the store state. * * This hook takes an optional equality comparison function as the second parameter * that allows you to customize the way the selected state is compared to determine * whether the component needs to be re-rendered. * * @param {Function} selector the selector function * @param {Function=} equalityFn the function that will be used to determine equality * * @returns {any} the selected state * * @example * * import React from 'react' * import { useSelector } from 'react-redux' * * export const CounterComponent = () => { * const counter = useSelector(state => state.counter) * return <div>{counter}</div> * } */ export const useSelector = /*#__PURE__*/ createSelectorHook()