From 9259b556569d35cda4f1a4c76dd591c148dda825 Mon Sep 17 00:00:00 2001 From: Oscar Bonelli Date: Fri, 12 Sep 2025 23:01:21 -0600 Subject: [PATCH 1/2] feat: optimize useSet and useMap hooks with useRef + reducer for performance --- src/useMap.ts | 77 +++++++++++++++++++++++++++++++++------------------ src/useSet.ts | 71 +++++++++++++++++++++++++++++++---------------- 2 files changed, 97 insertions(+), 51 deletions(-) diff --git a/src/useMap.ts b/src/useMap.ts index ded74ed239..90fb68a3e2 100644 --- a/src/useMap.ts +++ b/src/useMap.ts @@ -1,47 +1,70 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo, useReducer, useRef } from 'react'; export interface StableActions { set: (key: K, value: T[K]) => void; setAll: (newMap: T) => void; remove: (key: K) => void; reset: () => void; + clear: () => void; } export interface Actions extends StableActions { get: (key: K) => T[K]; + has: (key: K) => boolean; } -const useMap = (initialMap: T = {} as T): [T, Actions] => { - const [map, set] = useState(initialMap); +const useMap = = Record>( + initialMap: T = {} as T +): [T, Actions] => { + // Keep the canonical object in a ref; re-render via counter + const initialRef = useRef(structuredClone ? structuredClone(initialMap) : { ...(initialMap as any) }); + const ref = useRef({ ...(initialMap as any) }); + const [, force] = useReducer((c: number) => c + 1, 0); - const stableActions = useMemo>( + const setKey = useCallback((key: K, value: T[K]) => { + (ref.current as any)[key] = value; + force(); + }, []); + + const setAll = useCallback((newMap: T) => { + ref.current = { ...(newMap as any) }; + force(); + }, []); + + const remove = useCallback((key: K) => { + if (key in ref.current) { + delete (ref.current as any)[key]; + force(); + } + }, []); + + const reset = useCallback(() => { + ref.current = { ...(initialRef.current as any) }; + force(); + }, []); + + const clear = useCallback(() => { + ref.current = {} as T; + force(); + }, []); + + const get = useCallback((key: K): T[K] => ref.current[key], []); + const has = useCallback((key: K) => key in ref.current, []); + + const stableActions = useMemo>( () => ({ - set: (key, entry) => { - set((prevMap) => ({ - ...prevMap, - [key]: entry, - })); - }, - setAll: (newMap: T) => { - set(newMap); - }, - remove: (key) => { - set((prevMap) => { - const { [key]: omit, ...rest } = prevMap; - return rest as T; - }); - }, - reset: () => set(initialMap), + set: setKey, + setAll, + remove, + reset, + clear, + get, + has, }), - [set] + [setKey, setAll, remove, reset, clear, get, has] ); - const utils = { - get: useCallback((key) => map[key], [map]), - ...stableActions, - } as Actions; - - return [map, utils]; + return [ref.current, stableActions]; }; export default useMap; diff --git a/src/useSet.ts b/src/useSet.ts index 9c88306cc9..795475cd42 100644 --- a/src/useSet.ts +++ b/src/useSet.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo, useReducer, useRef } from 'react'; export interface StableActions { add: (key: K) => void; @@ -10,31 +10,54 @@ export interface StableActions { export interface Actions extends StableActions { has: (key: K) => boolean; + size: () => number; + toArray: () => K[]; } -const useSet = (initialSet = new Set()): [Set, Actions] => { - const [set, setSet] = useState(initialSet); - - const stableActions = useMemo>(() => { - const add = (item: K) => setSet((prevSet) => new Set([...Array.from(prevSet), item])); - const remove = (item: K) => - setSet((prevSet) => new Set(Array.from(prevSet).filter((i) => i !== item))); - const toggle = (item: K) => - setSet((prevSet) => - prevSet.has(item) - ? new Set(Array.from(prevSet).filter((i) => i !== item)) - : new Set([...Array.from(prevSet), item]) - ); - - return { add, remove, toggle, reset: () => setSet(initialSet), clear: () => setSet(new Set()) }; - }, [setSet]); - - const utils = { - has: useCallback((item) => set.has(item), [set]), - ...stableActions, - } as Actions; - - return [set, utils]; +const useSet = (initial: Iterable = []): [Set, Actions] => { + const initialSet = useMemo(() => new Set(initial), []); // stable snapshot + const ref = useRef>(new Set(initialSet)); + const [, force] = useReducer((c: number) => c + 1, 0); + + const add = useCallback((item: K) => { + if (!ref.current.has(item)) { + ref.current.add(item); + force(); + } + }, []); + + const remove = useCallback((item: K) => { + if (ref.current.delete(item)) force(); + }, []); + + const toggle = useCallback((item: K) => { + if (ref.current.has(item)) ref.current.delete(item); + else ref.current.add(item); + force(); + }, []); + + const reset = useCallback(() => { + ref.current = new Set(initialSet); + force(); + }, [initialSet]); + + const clear = useCallback(() => { + if (ref.current.size) { + ref.current.clear(); + force(); + } + }, []); + + const has = useCallback((item: K) => ref.current.has(item), []); + const size = useCallback(() => ref.current.size, []); + const toArray = useCallback(() => Array.from(ref.current), []); + + const utils = useMemo>( + () => ({ add, remove, toggle, reset, clear, has, size, toArray }), + [add, remove, toggle, reset, clear, has, size, toArray] + ); + + return [ref.current, utils]; }; export default useSet; From 5b396ef67f81e0c1d9cbd7e451c1fce61b5de1a2 Mon Sep 17 00:00:00 2001 From: Oscar Bonelli Date: Fri, 12 Sep 2025 23:06:12 -0600 Subject: [PATCH 2/2] fix(useMap): match expected API {get,set,setAll,remove,reset} and remove extras --- src/useMap.ts | 27 +++++---------------------- src/useSet.ts | 14 ++++++-------- 2 files changed, 11 insertions(+), 30 deletions(-) diff --git a/src/useMap.ts b/src/useMap.ts index 90fb68a3e2..d768318657 100644 --- a/src/useMap.ts +++ b/src/useMap.ts @@ -5,19 +5,16 @@ export interface StableActions { setAll: (newMap: T) => void; remove: (key: K) => void; reset: () => void; - clear: () => void; } export interface Actions extends StableActions { get: (key: K) => T[K]; - has: (key: K) => boolean; } const useMap = = Record>( initialMap: T = {} as T ): [T, Actions] => { - // Keep the canonical object in a ref; re-render via counter - const initialRef = useRef(structuredClone ? structuredClone(initialMap) : { ...(initialMap as any) }); + const initialRef = useRef({ ...(initialMap as any) }); const ref = useRef({ ...(initialMap as any) }); const [, force] = useReducer((c: number) => c + 1, 0); @@ -43,28 +40,14 @@ const useMap = = Record>( force(); }, []); - const clear = useCallback(() => { - ref.current = {} as T; - force(); - }, []); - const get = useCallback((key: K): T[K] => ref.current[key], []); - const has = useCallback((key: K) => key in ref.current, []); - const stableActions = useMemo>( - () => ({ - set: setKey, - setAll, - remove, - reset, - clear, - get, - has, - }), - [setKey, setAll, remove, reset, clear, get, has] + const utils = useMemo>( + () => ({ set: setKey, setAll, remove, reset, get }), + [setKey, setAll, remove, reset, get] ); - return [ref.current, stableActions]; + return [ref.current, utils]; }; export default useMap; diff --git a/src/useSet.ts b/src/useSet.ts index 795475cd42..e20286f838 100644 --- a/src/useSet.ts +++ b/src/useSet.ts @@ -10,12 +10,10 @@ export interface StableActions { export interface Actions extends StableActions { has: (key: K) => boolean; - size: () => number; - toArray: () => K[]; } const useSet = (initial: Iterable = []): [Set, Actions] => { - const initialSet = useMemo(() => new Set(initial), []); // stable snapshot + const initialSet = useMemo(() => new Set(initial), []); const ref = useRef>(new Set(initialSet)); const [, force] = useReducer((c: number) => c + 1, 0); @@ -27,7 +25,9 @@ const useSet = (initial: Iterable = []): [Set, Actions] => { }, []); const remove = useCallback((item: K) => { - if (ref.current.delete(item)) force(); + if (ref.current.delete(item)) { + force(); + } }, []); const toggle = useCallback((item: K) => { @@ -49,12 +49,10 @@ const useSet = (initial: Iterable = []): [Set, Actions] => { }, []); const has = useCallback((item: K) => ref.current.has(item), []); - const size = useCallback(() => ref.current.size, []); - const toArray = useCallback(() => Array.from(ref.current), []); const utils = useMemo>( - () => ({ add, remove, toggle, reset, clear, has, size, toArray }), - [add, remove, toggle, reset, clear, has, size, toArray] + () => ({ add, remove, toggle, reset, clear, has }), + [add, remove, toggle, reset, clear, has] ); return [ref.current, utils];