| [a762898] | 1 | //import * as React from 'react'
|
|---|
| 2 | import { React } from '../utils/react'
|
|---|
| 3 | import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector.js'
|
|---|
| 4 | import type { ReactReduxContextValue } from '../components/Context'
|
|---|
| 5 | import { ReactReduxContext } from '../components/Context'
|
|---|
| 6 | import type { EqualityFn, NoInfer } from '../types'
|
|---|
| 7 | import {
|
|---|
| 8 | createReduxContextHook,
|
|---|
| 9 | useReduxContext as useDefaultReduxContext,
|
|---|
| 10 | } from './useReduxContext'
|
|---|
| 11 |
|
|---|
| 12 | /**
|
|---|
| 13 | * The frequency of development mode checks.
|
|---|
| 14 | *
|
|---|
| 15 | * @since 8.1.0
|
|---|
| 16 | * @internal
|
|---|
| 17 | */
|
|---|
| 18 | export type DevModeCheckFrequency = 'never' | 'once' | 'always'
|
|---|
| 19 |
|
|---|
| 20 | /**
|
|---|
| 21 | * Represents the configuration for development mode checks.
|
|---|
| 22 | *
|
|---|
| 23 | * @since 9.0.0
|
|---|
| 24 | * @internal
|
|---|
| 25 | */
|
|---|
| 26 | export interface DevModeChecks {
|
|---|
| 27 | /**
|
|---|
| 28 | * Overrides the global stability check for the selector.
|
|---|
| 29 | * - `once` - Run only the first time the selector is called.
|
|---|
| 30 | * - `always` - Run every time the selector is called.
|
|---|
| 31 | * - `never` - Never run the stability check.
|
|---|
| 32 | *
|
|---|
| 33 | * @default 'once'
|
|---|
| 34 | *
|
|---|
| 35 | * @since 8.1.0
|
|---|
| 36 | */
|
|---|
| 37 | stabilityCheck: DevModeCheckFrequency
|
|---|
| 38 |
|
|---|
| 39 | /**
|
|---|
| 40 | * Overrides the global identity function check for the selector.
|
|---|
| 41 | * - `once` - Run only the first time the selector is called.
|
|---|
| 42 | * - `always` - Run every time the selector is called.
|
|---|
| 43 | * - `never` - Never run the identity function check.
|
|---|
| 44 | *
|
|---|
| 45 | * **Note**: Previously referred to as `noopCheck`.
|
|---|
| 46 | *
|
|---|
| 47 | * @default 'once'
|
|---|
| 48 | *
|
|---|
| 49 | * @since 9.0.0
|
|---|
| 50 | */
|
|---|
| 51 | identityFunctionCheck: DevModeCheckFrequency
|
|---|
| 52 | }
|
|---|
| 53 |
|
|---|
| 54 | export interface UseSelectorOptions<Selected = unknown> {
|
|---|
| 55 | equalityFn?: EqualityFn<Selected>
|
|---|
| 56 |
|
|---|
| 57 | /**
|
|---|
| 58 | * `useSelector` performs additional checks in development mode to help
|
|---|
| 59 | * identify and warn about potential issues in selector behavior. This
|
|---|
| 60 | * option allows you to customize the behavior of these checks per selector.
|
|---|
| 61 | *
|
|---|
| 62 | * @since 9.0.0
|
|---|
| 63 | */
|
|---|
| 64 | devModeChecks?: Partial<DevModeChecks>
|
|---|
| 65 | }
|
|---|
| 66 |
|
|---|
| 67 | /**
|
|---|
| 68 | * Represents a custom hook that allows you to extract data from the
|
|---|
| 69 | * Redux store state, using a selector function. The selector function
|
|---|
| 70 | * takes the current state as an argument and returns a part of the state
|
|---|
| 71 | * or some derived data. The hook also supports an optional equality
|
|---|
| 72 | * function or options object to customize its behavior.
|
|---|
| 73 | *
|
|---|
| 74 | * @template StateType - The specific type of state this hook operates on.
|
|---|
| 75 | *
|
|---|
| 76 | * @public
|
|---|
| 77 | */
|
|---|
| 78 | export interface UseSelector<StateType = unknown> {
|
|---|
| 79 | /**
|
|---|
| 80 | * A function that takes a selector function as its first argument.
|
|---|
| 81 | * The selector function is responsible for selecting a part of
|
|---|
| 82 | * the Redux store's state or computing derived data.
|
|---|
| 83 | *
|
|---|
| 84 | * @param selector - A function that receives the current state and returns a part of the state or some derived data.
|
|---|
| 85 | * @param equalityFnOrOptions - An optional equality function or options object for customizing the behavior of the selector.
|
|---|
| 86 | * @returns The selected part of the state or derived data.
|
|---|
| 87 | *
|
|---|
| 88 | * @template TState - The specific type of state this hook operates on.
|
|---|
| 89 | * @template Selected - The type of the value that the selector function will return.
|
|---|
| 90 | */
|
|---|
| 91 | <TState extends StateType = StateType, Selected = unknown>(
|
|---|
| 92 | selector: (state: TState) => Selected,
|
|---|
| 93 | equalityFnOrOptions?: EqualityFn<Selected> | UseSelectorOptions<Selected>,
|
|---|
| 94 | ): Selected
|
|---|
| 95 |
|
|---|
| 96 | /**
|
|---|
| 97 | * Creates a "pre-typed" version of {@linkcode useSelector useSelector}
|
|---|
| 98 | * where the `state` type is predefined.
|
|---|
| 99 | *
|
|---|
| 100 | * This allows you to set the `state` type once, eliminating the need to
|
|---|
| 101 | * specify it with every {@linkcode useSelector useSelector} call.
|
|---|
| 102 | *
|
|---|
| 103 | * @returns A pre-typed `useSelector` with the state type already defined.
|
|---|
| 104 | *
|
|---|
| 105 | * @example
|
|---|
| 106 | * ```ts
|
|---|
| 107 | * export const useAppSelector = useSelector.withTypes<RootState>()
|
|---|
| 108 | * ```
|
|---|
| 109 | *
|
|---|
| 110 | * @template OverrideStateType - The specific type of state this hook operates on.
|
|---|
| 111 | *
|
|---|
| 112 | * @since 9.1.0
|
|---|
| 113 | */
|
|---|
| 114 | withTypes: <
|
|---|
| 115 | OverrideStateType extends StateType,
|
|---|
| 116 | >() => UseSelector<OverrideStateType>
|
|---|
| 117 | }
|
|---|
| 118 |
|
|---|
| 119 | const refEquality: EqualityFn<any> = (a, b) => a === b
|
|---|
| 120 |
|
|---|
| 121 | /**
|
|---|
| 122 | * Hook factory, which creates a `useSelector` hook bound to a given context.
|
|---|
| 123 | *
|
|---|
| 124 | * @param {React.Context} [context=ReactReduxContext] Context passed to your `<Provider>`.
|
|---|
| 125 | * @returns {Function} A `useSelector` hook bound to the specified context.
|
|---|
| 126 | */
|
|---|
| 127 | export function createSelectorHook(
|
|---|
| 128 | context: React.Context<ReactReduxContextValue<
|
|---|
| 129 | any,
|
|---|
| 130 | any
|
|---|
| 131 | > | null> = ReactReduxContext,
|
|---|
| 132 | ): UseSelector {
|
|---|
| 133 | const useReduxContext =
|
|---|
| 134 | context === ReactReduxContext
|
|---|
| 135 | ? useDefaultReduxContext
|
|---|
| 136 | : createReduxContextHook(context)
|
|---|
| 137 |
|
|---|
| 138 | const useSelector = <TState, Selected>(
|
|---|
| 139 | selector: (state: TState) => Selected,
|
|---|
| 140 | equalityFnOrOptions:
|
|---|
| 141 | | EqualityFn<NoInfer<Selected>>
|
|---|
| 142 | | UseSelectorOptions<NoInfer<Selected>> = {},
|
|---|
| 143 | ): Selected => {
|
|---|
| 144 | const { equalityFn = refEquality } =
|
|---|
| 145 | typeof equalityFnOrOptions === 'function'
|
|---|
| 146 | ? { equalityFn: equalityFnOrOptions }
|
|---|
| 147 | : equalityFnOrOptions
|
|---|
| 148 | if (process.env.NODE_ENV !== 'production') {
|
|---|
| 149 | if (!selector) {
|
|---|
| 150 | throw new Error(`You must pass a selector to useSelector`)
|
|---|
| 151 | }
|
|---|
| 152 | if (typeof selector !== 'function') {
|
|---|
| 153 | throw new Error(`You must pass a function as a selector to useSelector`)
|
|---|
| 154 | }
|
|---|
| 155 | if (typeof equalityFn !== 'function') {
|
|---|
| 156 | throw new Error(
|
|---|
| 157 | `You must pass a function as an equality function to useSelector`,
|
|---|
| 158 | )
|
|---|
| 159 | }
|
|---|
| 160 | }
|
|---|
| 161 |
|
|---|
| 162 | const reduxContext = useReduxContext()
|
|---|
| 163 |
|
|---|
| 164 | const { store, subscription, getServerState } = reduxContext
|
|---|
| 165 |
|
|---|
| 166 | const firstRun = React.useRef(true)
|
|---|
| 167 |
|
|---|
| 168 | const wrappedSelector = React.useCallback<typeof selector>(
|
|---|
| 169 | {
|
|---|
| 170 | [selector.name](state: TState) {
|
|---|
| 171 | const selected = selector(state)
|
|---|
| 172 | if (process.env.NODE_ENV !== 'production') {
|
|---|
| 173 | const { devModeChecks = {} } =
|
|---|
| 174 | typeof equalityFnOrOptions === 'function'
|
|---|
| 175 | ? {}
|
|---|
| 176 | : equalityFnOrOptions
|
|---|
| 177 | const { identityFunctionCheck, stabilityCheck } = reduxContext
|
|---|
| 178 | const {
|
|---|
| 179 | identityFunctionCheck: finalIdentityFunctionCheck,
|
|---|
| 180 | stabilityCheck: finalStabilityCheck,
|
|---|
| 181 | } = {
|
|---|
| 182 | stabilityCheck,
|
|---|
| 183 | identityFunctionCheck,
|
|---|
| 184 | ...devModeChecks,
|
|---|
| 185 | }
|
|---|
| 186 | if (
|
|---|
| 187 | finalStabilityCheck === 'always' ||
|
|---|
| 188 | (finalStabilityCheck === 'once' && firstRun.current)
|
|---|
| 189 | ) {
|
|---|
| 190 | const toCompare = selector(state)
|
|---|
| 191 | if (!equalityFn(selected, toCompare)) {
|
|---|
| 192 | let stack: string | undefined = undefined
|
|---|
| 193 | try {
|
|---|
| 194 | throw new Error()
|
|---|
| 195 | } catch (e) {
|
|---|
| 196 | // eslint-disable-next-line no-extra-semi
|
|---|
| 197 | ;({ stack } = e as Error)
|
|---|
| 198 | }
|
|---|
| 199 | console.warn(
|
|---|
| 200 | 'Selector ' +
|
|---|
| 201 | (selector.name || 'unknown') +
|
|---|
| 202 | ' returned a different result when called with the same parameters. This can lead to unnecessary rerenders.' +
|
|---|
| 203 | '\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',
|
|---|
| 204 | {
|
|---|
| 205 | state,
|
|---|
| 206 | selected,
|
|---|
| 207 | selected2: toCompare,
|
|---|
| 208 | stack,
|
|---|
| 209 | },
|
|---|
| 210 | )
|
|---|
| 211 | }
|
|---|
| 212 | }
|
|---|
| 213 | if (
|
|---|
| 214 | finalIdentityFunctionCheck === 'always' ||
|
|---|
| 215 | (finalIdentityFunctionCheck === 'once' && firstRun.current)
|
|---|
| 216 | ) {
|
|---|
| 217 | // @ts-ignore
|
|---|
| 218 | if (selected === state) {
|
|---|
| 219 | let stack: string | undefined = undefined
|
|---|
| 220 | try {
|
|---|
| 221 | throw new Error()
|
|---|
| 222 | } catch (e) {
|
|---|
| 223 | // eslint-disable-next-line no-extra-semi
|
|---|
| 224 | ;({ stack } = e as Error)
|
|---|
| 225 | }
|
|---|
| 226 | console.warn(
|
|---|
| 227 | 'Selector ' +
|
|---|
| 228 | (selector.name || 'unknown') +
|
|---|
| 229 | ' returned the root state when called. This can lead to unnecessary rerenders.' +
|
|---|
| 230 | '\nSelectors that return the entire state are almost certainly a mistake, as they will cause a rerender whenever *anything* in state changes.',
|
|---|
| 231 | { stack },
|
|---|
| 232 | )
|
|---|
| 233 | }
|
|---|
| 234 | }
|
|---|
| 235 | if (firstRun.current) firstRun.current = false
|
|---|
| 236 | }
|
|---|
| 237 | return selected
|
|---|
| 238 | },
|
|---|
| 239 | }[selector.name],
|
|---|
| 240 | [selector],
|
|---|
| 241 | )
|
|---|
| 242 |
|
|---|
| 243 | const selectedState = useSyncExternalStoreWithSelector(
|
|---|
| 244 | subscription.addNestedSub,
|
|---|
| 245 | store.getState,
|
|---|
| 246 | getServerState || store.getState,
|
|---|
| 247 | wrappedSelector,
|
|---|
| 248 | equalityFn,
|
|---|
| 249 | )
|
|---|
| 250 |
|
|---|
| 251 | React.useDebugValue(selectedState)
|
|---|
| 252 |
|
|---|
| 253 | return selectedState
|
|---|
| 254 | }
|
|---|
| 255 |
|
|---|
| 256 | Object.assign(useSelector, {
|
|---|
| 257 | withTypes: () => useSelector,
|
|---|
| 258 | })
|
|---|
| 259 |
|
|---|
| 260 | return useSelector as UseSelector
|
|---|
| 261 | }
|
|---|
| 262 |
|
|---|
| 263 | /**
|
|---|
| 264 | * A hook to access the redux store's state. This hook takes a selector function
|
|---|
| 265 | * as an argument. The selector is called with the store state.
|
|---|
| 266 | *
|
|---|
| 267 | * This hook takes an optional equality comparison function as the second parameter
|
|---|
| 268 | * that allows you to customize the way the selected state is compared to determine
|
|---|
| 269 | * whether the component needs to be re-rendered.
|
|---|
| 270 | *
|
|---|
| 271 | * @param {Function} selector the selector function
|
|---|
| 272 | * @param {Function=} equalityFn the function that will be used to determine equality
|
|---|
| 273 | *
|
|---|
| 274 | * @returns {any} the selected state
|
|---|
| 275 | *
|
|---|
| 276 | * @example
|
|---|
| 277 | *
|
|---|
| 278 | * import React from 'react'
|
|---|
| 279 | * import { useSelector } from 'react-redux'
|
|---|
| 280 | *
|
|---|
| 281 | * export const CounterComponent = () => {
|
|---|
| 282 | * const counter = useSelector(state => state.counter)
|
|---|
| 283 | * return <div>{counter}</div>
|
|---|
| 284 | * }
|
|---|
| 285 | */
|
|---|
| 286 | export const useSelector = /*#__PURE__*/ createSelectorHook()
|
|---|