Skip to content

Rewrite useSet and useMap to avoid building a new instance on each update - #2687

Open
wahab-cide wants to merge 4 commits into
streamich:masterfrom
wahab-cide:master
Open

Rewrite useSet and useMap to avoid building a new instance on each update#2687
wahab-cide wants to merge 4 commits into
streamich:masterfrom
wahab-cide:master

Conversation

@wahab-cide

Copy link
Copy Markdown

Description

This PR optimizes useSet and useMap hooks to avoid inefficient array/object conversions on every operation. Addresses issue #1188.

Problem

The original implementations had performance issues:

useSet

  • Before:new Set([...Array.from(prevSet), item]) - O(n) array conversion for O(1) Set operations
  • After: Immutable state updates with native Set methods - O(n) only on actual mutations

useMap

  • Before: Object spreading in every state update
  • After: Optimized immutable updates with early returns to prevent unnecessary re-renders

Solution

Refactored both hooks to use immutable state updates with smart optimizations:

  1. Early returns - Skip re-renders when operations don't change data

    // useSet add()if(prev.has(item))returnprev;// No re-render if item exists
  2. Proper memoization - Utils object stability for React.memo compatibility

    constutils=useMemo(()=>({ has, ...stableActions}),[has,stableActions]);
  3. Dependency fixes - initialSet/initialMap properly tracked for reset()

  4. Robustness improvements:

    • Object.is instead of === for correct NaN handling
    • Lazy initializers with defensive copies
    • Better TypeScript generics for type inference

Performance Impact

OperationBeforeAfterImprovement
useSet mutationsO(n) array conversionO(n) Set copy on changeSame complexity, better constants
useSet no-opsO(n) with re-renderO(1) no re-renderSignificant improvement
useMap mutationsO(n) spreadO(n) spread on changeSame
useMap no-opsO(n) with re-renderO(1) no re-renderSignificant improvement
Component re-rendersO(1)O(1)Same

Breaking Changes

None. This is a performance optimization with full backward compatibility:

  • Same API signatures
  • Same behavior
  • All existing tests pass without modification
  • Action functions remain stable (critical for React.memo)

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality) - Performance optimization
  • Breaking change(fix or feature that would cause existing functionality to not work as before)

Checklist

  • Read the Contributing Guide
  • Perform a code self-review
  • Comment the code, particularly in hard-to-understand areas
  • Add documentation (No API changes - existing docs still valid)
  • Add hook's story at Storybook (no new hooks, no API changes)
  • Cover changes with tests (All existing tests pass - 26/26)
  • Ensure the test suite passes (yarn test)
  • Provide 100% tests coverage (Existing tests already provide full coverage)
  • Make sure code lints (yarn lint). Fix it with yarn lint:fix in case of failure.
  • Make sure types are fine (yarn lint:types).

Test Results

PASS tests/useMap.test.ts
PASS tests/useSet.test.ts
Test Suites: 2 passed, 2 total
Tests: 26 passed, 26 total

Related Issues

Fixes#1188

 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.
Fixesstreamich#1188
 - 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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Idea: rewrite useSet and useMap to avoid building a new instance on each update

1 participant

@wahab-cide