|
| 1 | +import { useReducer, useRef, useEffect, useMemo, useLayoutEffect } from 'react' |
| 2 | +import invariant from 'invariant' |
| 3 | +import { useReduxContext } from './useReduxContext' |
| 4 | +import shallowEqual from '../utils/shallowEqual' |
| 5 | +import Subscription from '../utils/Subscription' |
| 6 | + |
| 7 | +// React currently throws a warning when using useLayoutEffect on the server. |
| 8 | +// To get around it, we can conditionally useEffect on the server (no-op) and |
| 9 | +// useLayoutEffect in the browser. We need useLayoutEffect to ensure the store |
| 10 | +// subscription callback always has the selector from the latest render commit |
| 11 | +// available, otherwise a store update may happen between render and the effect, |
| 12 | +// which may cause missed updates; we also must ensure the store subscription |
| 13 | +// is created synchronously, otherwise a store update may occur before the |
| 14 | +// subscription is created and an inconsistent state may be observed |
| 15 | +const useIsomorphicLayoutEffect = |
| 16 | + typeof window !== 'undefined' ? useLayoutEffect : useEffect |
| 17 | + |
| 18 | +/** |
| 19 | + * A hook to access the redux store's state. This hook takes a selector function |
| 20 | + * as an argument. The selector is called with the store state. |
| 21 | + * |
| 22 | + * @param {Function} selector the selector function |
| 23 | + * |
| 24 | + * @returns {any} the selected state |
| 25 | + * |
| 26 | + * Usage: |
| 27 | + * |
| 28 | +```jsx |
| 29 | +import React from 'react' |
| 30 | +import { useSelector } from 'react-redux' |
| 31 | +
|
| 32 | +export const CounterComponent = () => { |
| 33 | + const counter = useSelector(state => state.counter) |
| 34 | + return <div>{counter}</div> |
| 35 | +} |
| 36 | +``` |
| 37 | + */ |
| 38 | +export function useSelector(selector) { |
| 39 | + invariant(selector, `You must pass a selector to useSelectors`) |
| 40 | + |
| 41 | + const { store, subscription: contextSub } = useReduxContext() |
| 42 | + const [, forceRender] = useReducer(s => s + 1, 0) |
| 43 | + |
| 44 | + const subscription = useMemo(() => new Subscription(store, contextSub), [ |
| 45 | + store, |
| 46 | + contextSub |
| 47 | + ]) |
| 48 | + |
| 49 | + const latestSubscriptionCallbackError = useRef() |
| 50 | + const latestSelector = useRef(selector) |
| 51 | + |
| 52 | + let selectedState = undefined |
| 53 | + |
| 54 | + try { |
| 55 | + selectedState = latestSelector.current(store.getState()) |
| 56 | + } catch (err) { |
| 57 | + let errorMessage = `An error occured while selecting the store state: ${ |
| 58 | + err.message |
| 59 | + }.` |
| 60 | + |
| 61 | + if (latestSubscriptionCallbackError.current) { |
| 62 | + errorMessage += `\nThe error may be correlated with this previous error:\n${ |
| 63 | + latestSubscriptionCallbackError.current.stack |
| 64 | + }\n\nOriginal stack trace:` |
| 65 | + } |
| 66 | + |
| 67 | + throw new Error(errorMessage) |
| 68 | + } |
| 69 | + |
| 70 | + const latestSelectedState = useRef(selectedState) |
| 71 | + |
| 72 | + useIsomorphicLayoutEffect(() => { |
| 73 | + latestSelector.current = selector |
| 74 | + latestSelectedState.current = selectedState |
| 75 | + latestSubscriptionCallbackError.current = undefined |
| 76 | + }) |
| 77 | + |
| 78 | + useIsomorphicLayoutEffect(() => { |
| 79 | + function checkForUpdates() { |
| 80 | + try { |
| 81 | + const newSelectedState = latestSelector.current(store.getState()) |
| 82 | + |
| 83 | + if (shallowEqual(newSelectedState, latestSelectedState.current)) { |
| 84 | + return |
| 85 | + } |
| 86 | + |
| 87 | + latestSelectedState.current = newSelectedState |
| 88 | + } catch (err) { |
| 89 | + // we ignore all errors here, since when the component |
| 90 | + // is re-rendered, the selectors are called again, and |
| 91 | + // will throw again, if neither props nor store state |
| 92 | + // changed |
| 93 | + latestSubscriptionCallbackError.current = err |
| 94 | + } |
| 95 | + |
| 96 | + forceRender({}) |
| 97 | + } |
| 98 | + |
| 99 | + subscription.onStateChange = checkForUpdates |
| 100 | + subscription.trySubscribe() |
| 101 | + |
| 102 | + checkForUpdates() |
| 103 | + |
| 104 | + return () => subscription.tryUnsubscribe() |
| 105 | + }, [store, subscription]) |
| 106 | + |
| 107 | + return selectedState |
| 108 | +} |
0 commit comments