Skip to content

Conversation

@wahab-cide
Copy link

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)) return prev;  // No re-render if item exists
  2. Proper memoization - Utils object stability for React.memo compatibility

    const utils = 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

Operation Before After Improvement
useSet mutations O(n) array conversion O(n) Set copy on change Same complexity, better constants
useSet no-ops O(n) with re-render O(1) no re-render Significant improvement
useMap mutations O(n) spread O(n) spread on change Same
useMap no-ops O(n) with re-render O(1) no re-render Significant improvement
Component re-renders O(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.

   Fixes streamich#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 free to 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