UNPKG

4.08 kBJavaScriptView Raw
1import { useReducer, useRef, useMemo, useContext } from 'react'
2import { useReduxContext as useDefaultReduxContext } from './useReduxContext'
3import Subscription from '../utils/Subscription'
4import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
5import { ReactReduxContext } from '../components/Context'
6
7const refEquality = (a, b) => a === b
8
9function useSelectorWithStoreAndSubscription(
10 selector,
11 equalityFn,
12 store,
13 contextSub
14) {
15 const [, forceRender] = useReducer(s => s + 1, 0)
16
17 const subscription = useMemo(() => new Subscription(store, contextSub), [
18 store,
19 contextSub
20 ])
21
22 const latestSubscriptionCallbackError = useRef()
23 const latestSelector = useRef()
24 const latestSelectedState = useRef()
25
26 let selectedState
27
28 try {
29 if (
30 selector !== latestSelector.current ||
31 latestSubscriptionCallbackError.current
32 ) {
33 selectedState = selector(store.getState())
34 } else {
35 selectedState = latestSelectedState.current
36 }
37 } catch (err) {
38 if (latestSubscriptionCallbackError.current) {
39 err.message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`
40 }
41
42 throw err
43 }
44
45 useIsomorphicLayoutEffect(() => {
46 latestSelector.current = selector
47 latestSelectedState.current = selectedState
48 latestSubscriptionCallbackError.current = undefined
49 })
50
51 useIsomorphicLayoutEffect(() => {
52 function checkForUpdates() {
53 try {
54 const newSelectedState = latestSelector.current(store.getState())
55
56 if (equalityFn(newSelectedState, latestSelectedState.current)) {
57 return
58 }
59
60 latestSelectedState.current = newSelectedState
61 } catch (err) {
62 // we ignore all errors here, since when the component
63 // is re-rendered, the selectors are called again, and
64 // will throw again, if neither props nor store state
65 // changed
66 latestSubscriptionCallbackError.current = err
67 }
68
69 forceRender({})
70 }
71
72 subscription.onStateChange = checkForUpdates
73 subscription.trySubscribe()
74
75 checkForUpdates()
76
77 return () => subscription.tryUnsubscribe()
78 }, [store, subscription])
79
80 return selectedState
81}
82
83/**
84 * Hook factory, which creates a `useSelector` hook bound to a given context.
85 *
86 * @param {React.Context} [context=ReactReduxContext] Context passed to your `<Provider>`.
87 * @returns {Function} A `useSelector` hook bound to the specified context.
88 */
89export function createSelectorHook(context = ReactReduxContext) {
90 const useReduxContext =
91 context === ReactReduxContext
92 ? useDefaultReduxContext
93 : () => useContext(context)
94 return function useSelector(selector, equalityFn = refEquality) {
95 if (process.env.NODE_ENV !== 'production' && !selector) {
96 throw new Error(`You must pass a selector to useSelectors`)
97 }
98 const { store, subscription: contextSub } = useReduxContext()
99
100 return useSelectorWithStoreAndSubscription(
101 selector,
102 equalityFn,
103 store,
104 contextSub
105 )
106 }
107}
108
109/**
110 * A hook to access the redux store's state. This hook takes a selector function
111 * as an argument. The selector is called with the store state.
112 *
113 * This hook takes an optional equality comparison function as the second parameter
114 * that allows you to customize the way the selected state is compared to determine
115 * whether the component needs to be re-rendered.
116 *
117 * @param {Function} selector the selector function
118 * @param {Function=} equalityFn the function that will be used to determine equality
119 *
120 * @returns {any} the selected state
121 *
122 * @example
123 *
124 * import React from 'react'
125 * import { useSelector } from 'react-redux'
126 *
127 * export const CounterComponent = () => {
128 * const counter = useSelector(state => state.counter)
129 * return <div>{counter}</div>
130 * }
131 */
132export const useSelector = /*#__PURE__*/ createSelectorHook()