158 lines
5.3 KiB
JavaScript
158 lines
5.3 KiB
JavaScript
import { useReducer, useRef, useMemo, useContext, useDebugValue } from 'react';
|
|
import { useReduxContext as useDefaultReduxContext } from './useReduxContext';
|
|
import { createSubscription } from '../utils/Subscription';
|
|
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect';
|
|
import { ReactReduxContext } from '../components/Context';
|
|
|
|
var refEquality = function refEquality(a, b) {
|
|
return a === b;
|
|
};
|
|
|
|
function useSelectorWithStoreAndSubscription(selector, equalityFn, store, contextSub) {
|
|
var _useReducer = useReducer(function (s) {
|
|
return s + 1;
|
|
}, 0),
|
|
forceRender = _useReducer[1];
|
|
|
|
var subscription = useMemo(function () {
|
|
return createSubscription(store, contextSub);
|
|
}, [store, contextSub]);
|
|
var latestSubscriptionCallbackError = useRef();
|
|
var latestSelector = useRef();
|
|
var latestStoreState = useRef();
|
|
var latestSelectedState = useRef();
|
|
var storeState = store.getState();
|
|
var selectedState;
|
|
|
|
try {
|
|
if (selector !== latestSelector.current || storeState !== latestStoreState.current || latestSubscriptionCallbackError.current) {
|
|
var newSelectedState = selector(storeState); // ensure latest selected state is reused so that a custom equality function can result in identical references
|
|
|
|
if (latestSelectedState.current === undefined || !equalityFn(newSelectedState, latestSelectedState.current)) {
|
|
selectedState = newSelectedState;
|
|
} else {
|
|
selectedState = latestSelectedState.current;
|
|
}
|
|
} else {
|
|
selectedState = latestSelectedState.current;
|
|
}
|
|
} catch (err) {
|
|
if (latestSubscriptionCallbackError.current) {
|
|
err.message += "\nThe error may be correlated with this previous error:\n" + latestSubscriptionCallbackError.current.stack + "\n\n";
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
|
|
useIsomorphicLayoutEffect(function () {
|
|
latestSelector.current = selector;
|
|
latestStoreState.current = storeState;
|
|
latestSelectedState.current = selectedState;
|
|
latestSubscriptionCallbackError.current = undefined;
|
|
});
|
|
useIsomorphicLayoutEffect(function () {
|
|
function checkForUpdates() {
|
|
try {
|
|
var newStoreState = store.getState(); // Avoid calling selector multiple times if the store's state has not changed
|
|
|
|
if (newStoreState === latestStoreState.current) {
|
|
return;
|
|
}
|
|
|
|
var _newSelectedState = latestSelector.current(newStoreState);
|
|
|
|
if (equalityFn(_newSelectedState, latestSelectedState.current)) {
|
|
return;
|
|
}
|
|
|
|
latestSelectedState.current = _newSelectedState;
|
|
latestStoreState.current = newStoreState;
|
|
} catch (err) {
|
|
// we ignore all errors here, since when the component
|
|
// is re-rendered, the selectors are called again, and
|
|
// will throw again, if neither props nor store state
|
|
// changed
|
|
latestSubscriptionCallbackError.current = err;
|
|
}
|
|
|
|
forceRender();
|
|
}
|
|
|
|
subscription.onStateChange = checkForUpdates;
|
|
subscription.trySubscribe();
|
|
checkForUpdates();
|
|
return function () {
|
|
return subscription.tryUnsubscribe();
|
|
};
|
|
}, [store, subscription]);
|
|
return selectedState;
|
|
}
|
|
/**
|
|
* 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) {
|
|
if (context === void 0) {
|
|
context = ReactReduxContext;
|
|
}
|
|
|
|
var useReduxContext = context === ReactReduxContext ? useDefaultReduxContext : function () {
|
|
return useContext(context);
|
|
};
|
|
return function useSelector(selector, equalityFn) {
|
|
if (equalityFn === void 0) {
|
|
equalityFn = refEquality;
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|
|
|
|
var _useReduxContext = useReduxContext(),
|
|
store = _useReduxContext.store,
|
|
contextSub = _useReduxContext.subscription;
|
|
|
|
var selectedState = useSelectorWithStoreAndSubscription(selector, equalityFn, store, contextSub);
|
|
useDebugValue(selectedState);
|
|
return selectedState;
|
|
};
|
|
}
|
|
/**
|
|
* 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 var useSelector = /*#__PURE__*/createSelectorHook(); |