import * as React from 'react'; import { Context, ContextSelector, ContextValue } from './types'; import { useIsomorphicLayoutEffect } from './utils'; type UseSelectorsRef< Value, Properties extends string, Selectors extends Record>, SelectedValue extends any > = { selectors: Selectors; value: Value; selected: Record; }; /** * This hook returns context selected value by selectors. * It will only accept context created by `createContext`. * It will trigger re-render if only the selected value is referencially changed. */ export const useContextSelectors = < Value, Properties extends string, Selectors extends Record>, SelectedValue extends any >( context: Context, selectors: Selectors, ): Record => { const { subscribe, value } = React.useContext((context as unknown) as Context>); const [, forceUpdate] = React.useReducer((c: number) => c + 1, 0) as [never, () => void]; const ref = React.useRef>(); const selected = {} as Record; Object.keys(selectors).forEach((key: Properties) => { selected[key] = selectors[key](value); }); useIsomorphicLayoutEffect(() => { ref.current = { selectors, value, selected, }; }); useIsomorphicLayoutEffect(() => { const callback = (nextState: Value) => { try { const reference: UseSelectorsRef = ref.current as NonNullable< UseSelectorsRef >; if ( reference.value === nextState || Object.keys(reference.selected).every((key: Properties) => Object.is(reference.selected[key], reference.selectors[key](nextState)), ) ) { // not changed return; } } catch (e) { // ignored (stale props or some other reason) } forceUpdate(); }; return subscribe(callback); }, [subscribe]); return selected; };