From 41caa6bd555faffac756d0f71ec2946395d68c6c Mon Sep 17 00:00:00 2001 From: Abdul Wahab Cide Ali Date: Mon, 15 Dec 2025 20:31:53 -0500 Subject: [PATCH 1/4] perf: optimize useSet and useMap to use native methods Replace O(n) array conversions in useSet with native Set operations (add, delete) to improve complexity to O(1). Refactor useMap to use ref-based state. Both hooks now use useRef with counter-based re-rendering and maintain full API compatibility. Fixes #1188 --- src/useMap.ts | 37 +++++++++++++++++++---------------- src/useSet.ts | 53 +++++++++++++++++++++++++++++++++++---------------- 2 files changed, 58 insertions(+), 32 deletions(-) diff --git a/src/useMap.ts b/src/useMap.ts index ded74ed239..31b57c2b8d 100644 --- a/src/useMap.ts +++ b/src/useMap.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo, useRef, useState } from 'react'; export interface StableActions { set: (key: K, value: T[K]) => void; @@ -12,36 +12,41 @@ export interface Actions extends StableActions { } const useMap = (initialMap: T = {} as T): [T, Actions] => { - const [map, set] = useState(initialMap); + const mapRef = useRef({ ...initialMap }); + const [, rerender] = useState(0); const stableActions = useMemo>( () => ({ - set: (key, entry) => { - set((prevMap) => ({ - ...prevMap, + set: (key: K, entry: T[K]) => { + mapRef.current = { + ...mapRef.current, [key]: entry, - })); + }; + rerender((c: number) => c + 1); }, setAll: (newMap: T) => { - set(newMap); + mapRef.current = newMap; + rerender((c: number) => c + 1); }, - remove: (key) => { - set((prevMap) => { - const { [key]: omit, ...rest } = prevMap; - return rest as T; - }); + remove: (key: K) => { + const { [key]: omit, ...rest } = mapRef.current; + mapRef.current = rest as T; + rerender((c: number) => c + 1); + }, + reset: () => { + mapRef.current = { ...initialMap }; + rerender((c: number) => c + 1); }, - reset: () => set(initialMap), }), - [set] + [] ); const utils = { - get: useCallback((key) => map[key], [map]), + get: useCallback((key: keyof T) => mapRef.current[key], []), ...stableActions, } as Actions; - return [map, utils]; + return [{ ...mapRef.current }, utils]; }; export default useMap; diff --git a/src/useSet.ts b/src/useSet.ts index 9c88306cc9..6cbc498790 100644 --- a/src/useSet.ts +++ b/src/useSet.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo, useRef, useState } from 'react'; export interface StableActions { add: (key: K) => void; @@ -13,28 +13,49 @@ export interface Actions extends StableActions { } const useSet = (initialSet = new Set()): [Set, Actions] => { - const [set, setSet] = useState(initialSet); + const setRef = useRef(new Set(initialSet)); + const [, rerender] = useState(0); 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 add = (item: K) => { + if (!setRef.current.has(item)) { + setRef.current.add(item); + rerender((c: number) => c + 1); + } + }; + const remove = (item: K) => { + if (setRef.current.delete(item)) { + rerender((c: number) => c + 1); + } + }; + const toggle = (item: K) => { + if (setRef.current.has(item)) { + setRef.current.delete(item); + } else { + setRef.current.add(item); + } + rerender((c: number) => c + 1); + }; + const reset = () => { + setRef.current = new Set(initialSet); + rerender((c: number) => c + 1); + }; + const clear = () => { + if (setRef.current.size > 0) { + setRef.current.clear(); + rerender((c: number) => c + 1); + } + }; + + return { add, remove, toggle, reset, clear }; + }, []); const utils = { - has: useCallback((item) => set.has(item), [set]), + has: useCallback((item: K) => setRef.current.has(item), []), ...stableActions, } as Actions; - return [set, utils]; + return [new Set(setRef.current), utils]; }; export default useSet; From d0e1ab5d91917140ec2f0bf3ebb63bf60f182372 Mon Sep 17 00:00:00 2001 From: Abdul Wahab Cide Ali Date: Mon, 15 Dec 2025 20:40:04 -0500 Subject: [PATCH 2/4] refactor: improve useSet and useMap to avoid O(n) cost on every render --- src/useMap.ts | 47 +++++++++++++++------------------- src/useSet.ts | 71 +++++++++++++++++++++++---------------------------- 2 files changed, 53 insertions(+), 65 deletions(-) diff --git a/src/useMap.ts b/src/useMap.ts index 31b57c2b8d..a6a849aa89 100644 --- a/src/useMap.ts +++ b/src/useMap.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; export interface StableActions { set: (key: K, value: T[K]) => void; @@ -12,41 +12,36 @@ export interface Actions extends StableActions { } const useMap = (initialMap: T = {} as T): [T, Actions] => { - const mapRef = useRef({ ...initialMap }); - const [, rerender] = useState(0); + const [map, setMap] = useState(initialMap); const stableActions = useMemo>( () => ({ - set: (key: K, entry: T[K]) => { - mapRef.current = { - ...mapRef.current, - [key]: entry, - }; - rerender((c: number) => c + 1); - }, - setAll: (newMap: T) => { - mapRef.current = newMap; - rerender((c: number) => c + 1); - }, - remove: (key: K) => { - const { [key]: omit, ...rest } = mapRef.current; - mapRef.current = rest as T; - rerender((c: number) => c + 1); - }, - reset: () => { - mapRef.current = { ...initialMap }; - rerender((c: number) => c + 1); - }, + set: (key: K, value: T[K]) => + setMap((prev) => { + if (prev[key] === value) return prev; + return { + ...prev, + [key]: value, + }; + }), + setAll: (newMap: T) => setMap(newMap), + remove: (key: K) => + setMap((prev) => { + if (!(key in prev)) return prev; + const { [key]: omit, ...rest } = prev; + return rest as T; + }), + reset: () => setMap(initialMap), }), - [] + [initialMap] ); const utils = { - get: useCallback((key: keyof T) => mapRef.current[key], []), + get: useCallback((key: keyof T) => map[key], [map]), ...stableActions, } as Actions; - return [{ ...mapRef.current }, utils]; + return [map, utils]; }; export default useMap; diff --git a/src/useSet.ts b/src/useSet.ts index 6cbc498790..f6b602f51d 100644 --- a/src/useSet.ts +++ b/src/useSet.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; export interface StableActions { add: (key: K) => void; @@ -13,49 +13,42 @@ export interface Actions extends StableActions { } const useSet = (initialSet = new Set()): [Set, Actions] => { - const setRef = useRef(new Set(initialSet)); - const [, rerender] = useState(0); - - const stableActions = useMemo>(() => { - const add = (item: K) => { - if (!setRef.current.has(item)) { - setRef.current.add(item); - rerender((c: number) => c + 1); - } - }; - const remove = (item: K) => { - if (setRef.current.delete(item)) { - rerender((c: number) => c + 1); - } - }; - const toggle = (item: K) => { - if (setRef.current.has(item)) { - setRef.current.delete(item); - } else { - setRef.current.add(item); - } - rerender((c: number) => c + 1); - }; - const reset = () => { - setRef.current = new Set(initialSet); - rerender((c: number) => c + 1); - }; - const clear = () => { - if (setRef.current.size > 0) { - setRef.current.clear(); - rerender((c: number) => c + 1); - } - }; - - return { add, remove, toggle, reset, clear }; - }, []); + const [set, setSet] = useState(() => new Set(initialSet)); + + const stableActions = useMemo>( + () => ({ + add: (item: K) => + setSet((prev) => { + if (prev.has(item)) return prev; + const next = new Set(prev); + next.add(item); + return next; + }), + remove: (item: K) => + setSet((prev) => { + if (!prev.has(item)) return prev; + const next = new Set(prev); + next.delete(item); + return next; + }), + toggle: (item: K) => + setSet((prev) => { + const next = new Set(prev); + prev.has(item) ? next.delete(item) : next.add(item); + return next; + }), + reset: () => setSet(new Set(initialSet)), + clear: () => setSet((prev) => (prev.size === 0 ? prev : new Set())), + }), + [initialSet] + ); const utils = { - has: useCallback((item: K) => setRef.current.has(item), []), + has: useCallback((item: K) => set.has(item), [set]), ...stableActions, } as Actions; - return [new Set(setRef.current), utils]; + return [set, utils]; }; export default useSet; From 5c26c3c8952ce5fbfbbe629d4ae29aaebfe3019a Mon Sep 17 00:00:00 2001 From: Abdul Wahab Cide Ali Date: Tue, 16 Dec 2025 01:42:15 -0500 Subject: [PATCH 3/4] refactor: add utils memoization and robustness improvements --- src/useMap.ts | 16 +++++++++------- src/useSet.ts | 10 ++++++---- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/useMap.ts b/src/useMap.ts index a6a849aa89..b915f74e6c 100644 --- a/src/useMap.ts +++ b/src/useMap.ts @@ -12,13 +12,13 @@ export interface Actions extends StableActions { } const useMap = (initialMap: T = {} as T): [T, Actions] => { - const [map, setMap] = useState(initialMap); + const [map, setMap] = useState(() => ({ ...initialMap })); const stableActions = useMemo>( () => ({ set: (key: K, value: T[K]) => setMap((prev) => { - if (prev[key] === value) return prev; + if (Object.is(prev[key], value)) return prev; return { ...prev, [key]: value, @@ -31,15 +31,17 @@ const useMap = (initialMap: T = {} as T): [T, Actions const { [key]: omit, ...rest } = prev; return rest as T; }), - reset: () => setMap(initialMap), + reset: () => setMap({ ...initialMap }), }), [initialMap] ); - const utils = { - get: useCallback((key: keyof T) => map[key], [map]), - ...stableActions, - } as Actions; + const get = useCallback((key: K): T[K] => map[key], [map]); + + const utils = useMemo>( + () => ({ get, ...stableActions }), + [get, stableActions] + ); return [map, utils]; }; diff --git a/src/useSet.ts b/src/useSet.ts index f6b602f51d..bac2deb9af 100644 --- a/src/useSet.ts +++ b/src/useSet.ts @@ -43,10 +43,12 @@ const useSet = (initialSet = new Set()): [Set, Actions] => { [initialSet] ); - const utils = { - has: useCallback((item: K) => set.has(item), [set]), - ...stableActions, - } as Actions; + const has = useCallback((item: K) => set.has(item), [set]); + + const utils = useMemo>( + () => ({ has, ...stableActions }), + [has, stableActions] + ); return [set, utils]; }; From e1c5ccf542b35323d80e3601ce47dcca6043d79c Mon Sep 17 00:00:00 2001 From: Abdul Wahab Cide Ali Date: Tue, 16 Dec 2025 02:09:11 -0500 Subject: [PATCH 4/4] docs: add comments - Add comment explaining Object.is usage for NaN handling in useMap - Add comment explaining utils memoization for stable references - Apply prettier formatting fixes All tests passing (26/26), lint clean, types clean. --- src/useMap.ts | 7 +++---- src/useSet.ts | 6 ++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/useMap.ts b/src/useMap.ts index b915f74e6c..b4419793f8 100644 --- a/src/useMap.ts +++ b/src/useMap.ts @@ -18,6 +18,7 @@ const useMap = (initialMap: T = {} as T): [T, Actions () => ({ set: (key: K, value: T[K]) => setMap((prev) => { + // Use Object.is for correct NaN handling if (Object.is(prev[key], value)) return prev; return { ...prev, @@ -38,10 +39,8 @@ const useMap = (initialMap: T = {} as T): [T, Actions const get = useCallback((key: K): T[K] => map[key], [map]); - const utils = useMemo>( - () => ({ get, ...stableActions }), - [get, stableActions] - ); + // Memoize the entire utils object to maintain stable reference + const utils = useMemo>(() => ({ get, ...stableActions }), [get, stableActions]); return [map, utils]; }; diff --git a/src/useSet.ts b/src/useSet.ts index bac2deb9af..47ef8e3cd1 100644 --- a/src/useSet.ts +++ b/src/useSet.ts @@ -45,10 +45,8 @@ const useSet = (initialSet = new Set()): [Set, Actions] => { const has = useCallback((item: K) => set.has(item), [set]); - const utils = useMemo>( - () => ({ has, ...stableActions }), - [has, stableActions] - ); + // Memoize the entire utils object to maintain stable reference + const utils = useMemo>(() => ({ has, ...stableActions }), [has, stableActions]); return [set, utils]; };