diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 695013a..1ccb038 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,12 +1,4 @@ export { createResolvedStoreHooks } from "./createResolvedStoreHooks"; export type { ResolvedStoreBindings, StorePlainHook, StoreValueHook } from "../types"; -export { useSelectorWithEquality } from "./useSelectorWithEquality"; -export type { - UseSelectorWithEqualityEqualityFn, - UseSelectorWithEqualityOptions, - UseSelectorWithEqualityReturn, - UseSelectorWithEqualitySelector, -} from "./useSelectorWithEquality"; export { createStoreSelectionBindings } from "./storeSelection"; -export { useStoreSelection, useStoreSelectionPlain } from "./storeSelection"; export type { StoreResolver, StoreSelectionBindings } from "./storeSelection"; diff --git a/src/hooks/storeSelection/createStoreSelectionBindings.ts b/src/hooks/storeSelection/createStoreSelectionBindings.ts index 6c6fa06..9042a5c 100644 --- a/src/hooks/storeSelection/createStoreSelectionBindings.ts +++ b/src/hooks/storeSelection/createStoreSelectionBindings.ts @@ -1,4 +1,4 @@ -import { useStoreSelection, useStoreSelectionPlain } from "./useStoreSelection"; +import { useStoreSelection, useStoreSelectionPlain } from "./storeSelection.hooks"; import type { StoreResolver, StoreSelectionBindings } from "./storeSelection.types"; export function createStoreSelectionBindings( diff --git a/src/hooks/storeSelection/index.ts b/src/hooks/storeSelection/index.ts index 3106814..0a0abb2 100644 --- a/src/hooks/storeSelection/index.ts +++ b/src/hooks/storeSelection/index.ts @@ -1,3 +1,2 @@ export { createStoreSelectionBindings } from "./createStoreSelectionBindings"; -export { useStoreSelection, useStoreSelectionPlain } from "./useStoreSelection"; export type { StoreResolver, StoreSelectionBindings } from "./storeSelection.types"; diff --git a/src/hooks/storeSelection/storeSelection.hooks.ts b/src/hooks/storeSelection/storeSelection.hooks.ts new file mode 100644 index 0000000..07cbb08 --- /dev/null +++ b/src/hooks/storeSelection/storeSelection.hooks.ts @@ -0,0 +1,56 @@ +import { useRef } from "react"; +import { useStore } from "zustand"; +import { shallow } from "zustand/shallow"; +import { identitySelector } from "./storeSelection.utils"; +import type { SelectionCache, SelectionStore } from "./storeSelection.types"; + +function useRetainedStoreSelection( + store: SelectionStore, + selector: (state: TState) => TSelected, + equalityFn: (left: TSelected, right: TSelected) => boolean +): TSelected { + const cacheRef = useRef>({ + hasValue: false, + store, + }); + + function retainEqualSelection(state: TState): TSelected { + const nextValue = selector(state); + const cache = cacheRef.current; + + if (cache.hasValue && Object.is(cache.store, store) && equalityFn(cache.value, nextValue)) { + return cache.value; + } + + cacheRef.current = { + hasValue: true, + store, + value: nextValue, + }; + + return nextValue; + } + + return useStore(store, retainEqualSelection); +} + +export function useStoreSelection( + store: SelectionStore, + selector?: (state: TState) => TSelected, + equalityFn?: (left: TSelected | TState, right: TSelected | TState) => boolean +): TSelected | TState { + return useRetainedStoreSelection( + store, + (state: TState): TSelected | TState => (selector ? selector(state) : state), + equalityFn ?? shallow + ); +} + +export function useStoreSelectionPlain( + store: SelectionStore, + selector?: (state: TState) => TSelected +): TSelected | TState { + const actualSelector = selector ?? identitySelector; + + return useStore(store, actualSelector); +} diff --git a/src/hooks/storeSelection/storeSelection.types.ts b/src/hooks/storeSelection/storeSelection.types.ts index 7e7a4c1..797132d 100644 --- a/src/hooks/storeSelection/storeSelection.types.ts +++ b/src/hooks/storeSelection/storeSelection.types.ts @@ -8,6 +8,19 @@ export type SelectionStore = Pick< export type StoreResolver = () => SelectionStore; +export interface EmptySelectionCache { + hasValue: false; + store: unknown; +} + +export interface RetainedSelectionCache { + hasValue: true; + store: unknown; + value: TSelected; +} + +export type SelectionCache = EmptySelectionCache | RetainedSelectionCache; + export interface StoreSelectionBindings { useStoreValue: StoreValueHook; useStorePlain: StorePlainHook; diff --git a/src/utils/identitySelector.ts b/src/hooks/storeSelection/storeSelection.utils.ts similarity index 100% rename from src/utils/identitySelector.ts rename to src/hooks/storeSelection/storeSelection.utils.ts diff --git a/src/hooks/storeSelection/useStoreSelection.ts b/src/hooks/storeSelection/useStoreSelection.ts deleted file mode 100644 index 7c3361d..0000000 --- a/src/hooks/storeSelection/useStoreSelection.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useStore } from "zustand"; -import { shallow } from "zustand/shallow"; -import { identitySelector } from "../../utils"; -import { useSelectorWithEquality } from "../useSelectorWithEquality"; -import type { SelectionStore } from "./storeSelection.types"; - -/** Selects from a store with store-scoped reference retention. */ -export function useStoreSelection( - store: SelectionStore, - selector?: (state: TState) => TSelected, - equalityFn?: (left: TSelected | TState, right: TSelected | TState) => boolean -): TSelected | TState { - const actualSelector = useSelectorWithEquality({ - cacheKey: store, - equalityFn: equalityFn ?? shallow, - selector: (state: TState): TSelected | TState => (selector ? selector(state) : state), - }); - - return useStore(store, actualSelector); -} - -/** Keeps plain selection independent of equality caching. */ -export function useStoreSelectionPlain( - store: SelectionStore, - selector?: (state: TState) => TSelected -): TSelected | TState { - const actualSelector = selector ?? identitySelector; - - return useStore(store, actualSelector); -} diff --git a/src/hooks/useSelectorWithEquality/index.ts b/src/hooks/useSelectorWithEquality/index.ts deleted file mode 100644 index 7bd3131..0000000 --- a/src/hooks/useSelectorWithEquality/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { useSelectorWithEquality } from "./useSelectorWithEquality"; -export type { - UseSelectorWithEqualityEqualityFn, - UseSelectorWithEqualityOptions, - UseSelectorWithEqualityReturn, - UseSelectorWithEqualitySelector, -} from "./useSelectorWithEquality.types"; diff --git a/src/hooks/useSelectorWithEquality/useSelectorWithEquality.ts b/src/hooks/useSelectorWithEquality/useSelectorWithEquality.ts deleted file mode 100644 index 167d5e6..0000000 --- a/src/hooks/useSelectorWithEquality/useSelectorWithEquality.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useRef } from "react"; -import type { - SelectorCache, - UseSelectorWithEqualityOptions, - UseSelectorWithEqualityReturn, -} from "./useSelectorWithEquality.types"; - -export function useSelectorWithEquality({ - cacheKey, - equalityFn, - selector, -}: UseSelectorWithEqualityOptions): UseSelectorWithEqualityReturn< - TState, - TSelected -> { - const cacheRef = useRef>({ - hasValue: false, - key: cacheKey, - }); - - return (state: TState): TSelected => { - const nextValue = selector(state); - const cache = cacheRef.current; - - if (cache.hasValue && Object.is(cache.key, cacheKey) && equalityFn(cache.value, nextValue)) { - return cache.value; - } - - cacheRef.current = { - hasValue: true, - key: cacheKey, - value: nextValue, - }; - return nextValue; - }; -} diff --git a/src/hooks/useSelectorWithEquality/useSelectorWithEquality.types.ts b/src/hooks/useSelectorWithEquality/useSelectorWithEquality.types.ts deleted file mode 100644 index b1be962..0000000 --- a/src/hooks/useSelectorWithEquality/useSelectorWithEquality.types.ts +++ /dev/null @@ -1,27 +0,0 @@ -export type UseSelectorWithEqualitySelector = (state: TState) => TSelected; - -export type UseSelectorWithEqualityEqualityFn = ( - left: TSelected, - right: TSelected -) => boolean; - -export interface UseSelectorWithEqualityOptions { - cacheKey: unknown; - equalityFn: UseSelectorWithEqualityEqualityFn; - selector: UseSelectorWithEqualitySelector; -} - -export type UseSelectorWithEqualityReturn = (state: TState) => TSelected; - -export interface EmptySelectorCache { - hasValue: false; - key: unknown; -} - -export interface SelectorValueCache { - hasValue: true; - key: unknown; - value: TSelected; -} - -export type SelectorCache = EmptySelectorCache | SelectorValueCache; diff --git a/src/utils/index.ts b/src/utils/index.ts deleted file mode 100644 index 8bbd4c8..0000000 --- a/src/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { identitySelector } from "./identitySelector";