Skip to content

Immutability and Comparison Integration

Comprehensive guide to understanding how Immer-based immutability and comparison logic work together in Context-Action stores for optimal performance and correctness.

Overview

Context-Action uses a dual-layer optimization system within each individual store:

  • Immer: For safe immutable updates (Copy-on-Write) per store
  • Comparison Logic: For change detection and re-render optimization per store

These two systems solve different problems and work together within each store independently, not as replacements for each other.

Architecture Overview

Two-Layer System Design

typescript
// Individual Store Update Flow (each store operates independently)
setValue(newValue) {
  // Layer 1: Immutability (Immer) - only for this store's value
  const safeValue = deepClone(newValue);  // Uses Immer internally
  
  // Layer 2: Change Detection (Comparison) - only for this store's value
  const hasChanged = this._compareValues(this._value, safeValue);
  
  if (hasChanged) {
    this._value = safeValue;
    this._notifyListeners();  // Triggers React re-renders for this store only
  }
}

Why Both Are Needed

SystemPurposeProblem SolvedScope
ImmerImmutabilityPrevents mutation bugs, ensures safe copiesPer individual store
ComparisonChange DetectionPrevents unnecessary re-renders, optimizes performancePer individual store

Immer Integration Details

1. Safe Immutable Updates

typescript
// packages/react/src/stores/utils/immutable.ts
export function deepClone<T>(value: T): T {
  try {
    // Immer's produce for immutable copies
    return produce(value, (draft) => {
      // Immer's Copy-on-Write optimization
      // Returns original if unchanged, new object if modified
    });
  } catch (error) {
    // Fallback for objects Immer can't handle
    return fallbackClone(value);
  }
}

2. Update Method with Immer

typescript
// Store.ts
update(updater: (current: T) => T): void {
  // Step 1: Create safe copy for updater function
  const safeCurrentValue = deepClone(this._value);
  
  // Step 2: Apply user's update function
  const updatedValue = updater(safeCurrentValue);
  
  // Step 3: Set value (triggers comparison logic)
  this.setValue(updatedValue);
}

Key Benefits:

  • Security: Updater functions cannot mutate internal state
  • Consistency: All updates go through the same validation pipeline
  • Performance: Immer's Copy-on-Write avoids unnecessary object creation

Comparison Logic Integration

1. Change Detection Pipeline

typescript
protected _compareValues(oldValue: T, newValue: T): boolean {
  try {
    // Priority 1: Custom comparator
    if (this.customComparator) {
      const areEqual = this.customComparator(oldValue, newValue);
      return !areEqual; // Invert: same=false, different=true
    }
    
    // Priority 2: Store-specific comparison options
    else if (this.comparisonOptions) {
      const areEqual = compareValues(oldValue, newValue, this.comparisonOptions);
      return !areEqual;
    }
    
    // Priority 3: Optimized fast comparison (default)
    else {
      const areEqual = fastCompare(oldValue, newValue);
      return !areEqual;
    }
  } catch (error) {
    // Fallback: Reference comparison
    return !Object.is(oldValue, newValue);
  }
}

2. Comparison Strategies

typescript
// Reference Comparison (Fastest)
Object.is(oldValue, newValue)

// Shallow Comparison (Balanced)
shallowEquals(oldValue, newValue)

// Deep Comparison (Most Accurate)
deepEquals(oldValue, newValue)

// Fast Comparison (Optimized Default)
fastCompare(oldValue, newValue) // JSON serialization optimization

Performance Characteristics

Immer Performance

typescript
// Immer Copy-on-Write Benefits
const original = { user: { name: 'John', age: 30 } };

// No changes - returns original reference
const unchanged = produce(original, draft => {
  // No mutations
});
console.log(unchanged === original); // true - same reference!

// With changes - creates new object only for changed parts
const changed = produce(original, draft => {
  draft.user.name = 'Jane';
});
console.log(changed === original); // false
console.log(changed.user === original.user); // false - user object changed

Comparison Performance

StrategySpeedMemoryUse Case
Reference⚡⚡⚡⚡⚡⚡Primitives, managed objects
Fast Compare⚡⚡⚡⚡General objects (default)
Shallow⚡⚡⚡⚡Objects with shallow changes
DeepComplex nested objects

Real-World Integration Examples

1. Optimal Store Configuration

typescript
const { Provider, useStore } = createStoreContext('OptimizedApp', {
  // Simple values - reference comparison (default)
  counter: 0,
  isLoading: false,
  
  // Objects with shallow changes - shallow comparison
  user: { 
    initialValue: { name: '', role: '' },
    strategy: 'shallow'
  },
  
  // Complex nested objects - custom comparison
  complexData: {
    initialValue: { /* complex structure */ },
    comparisonOptions: {
      strategy: 'custom',
      customComparator: (prev, next) => {
        // Only compare specific fields that matter
        return prev.importantField === next.importantField;
      }
    }
  }
});

2. Update Patterns with Immer Benefits

typescript
function UserProfile() {
  const userStore = useStore('user');
  const user = useStoreValue(userStore);
  
  const updateUserProfile = useCallback((updates) => {
    // This is safe and optimized:
    userStore.update(current => ({
      ...current,
      profile: {
        ...current.profile,
        ...updates
      }
    }));
    
    // Behind the scenes:
    // 1. Immer creates safe copy of current value
    // 2. Update function runs on safe copy
    // 3. Result goes through comparison logic
    // 4. Only re-renders if actually changed
  }, [userStore]);
  
  return (
    <div>
      <h1>{user.profile.name}</h1>
      <button onClick={() => updateUserProfile({ name: 'New Name' })}>
        Update Name
      </button>
    </div>
  );
}

3. Performance-Optimized Cart Example

typescript
function ShoppingCart() {
  const cartStore = useStore('cart');
  
  // Optimized: Only re-renders when cart contents actually change
  const cartItems = useStoreValue(cartStore, cart => cart.items);
  const totalPrice = useStoreValue(cartStore, cart => 
    cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  );
  
  const addItem = useCallback((item) => {
    cartStore.update(current => {
      // Immer ensures this is safe
      const existingItem = current.items.find(i => i.id === item.id);
      
      if (existingItem) {
        // Update existing item quantity
        return {
          ...current,
          items: current.items.map(i => 
            i.id === item.id 
              ? { ...i, quantity: i.quantity + 1 }
              : i
          )
        };
      } else {
        // Add new item
        return {
          ...current,
          items: [...current.items, { ...item, quantity: 1 }]
        };
      }
    });
    // Comparison logic ensures re-render only if cart actually changed
  }, [cartStore]);
  
  return (
    <div>
      <div>Items: {cartItems.length}</div>
      <div>Total: ${totalPrice.toFixed(2)}</div>
      {/* ... */}
    </div>
  );
}

Common Misconceptions

❌ "Immer replaces comparison logic"

Wrong assumption:

typescript
// "Since Immer handles immutability, we don't need comparison"
setValue(newValue) {
  this._value = produce(newValue, draft => {});
  this._notifyListeners(); // Always triggers re-render!
}

Why it's wrong:

  • Would trigger re-render even when value hasn't changed
  • No performance optimization for identical values
  • Breaks React's optimization assumptions

✅ Correct understanding:

typescript
// Immer + Comparison = Optimal performance
setValue(newValue) {
  const safeValue = produce(newValue, draft => {}); // Immutability
  
  if (this._compareValues(this._value, safeValue)) {  // Change detection
    this._value = safeValue;
    this._notifyListeners(); // Only when actually changed
  }
}

❌ "Comparison logic is redundant with Immer"

Wrong assumption:

  • "Immer returns same reference if unchanged, so comparison is not needed"

Why it's wrong:

  • Only true for update() method with no-op functions
  • setValue() with identical values still needs comparison
  • Custom comparators enable business-logic-specific optimizations

Advanced Integration Patterns

1. Custom Comparators with Immer

typescript
const store = useStore('data');

// Set custom comparison that works with Immer
store.setComparisonOptions({
  strategy: 'custom',
  customComparator: (prev, next) => {
    // Even with Immer ensuring immutability,
    // we can optimize based on business logic
    return (
      prev.lastModified === next.lastModified &&
      prev.version === next.version
    );
  }
});

2. Debugging Integration

typescript
// Enable debugging to see both systems working
const debugStore = useStoreValue(store, undefined, {
  debug: true,
  debugName: 'MyStore'
});

// Console output shows:
// [MyStore] Immer: Created safe copy
// [MyStore] Comparison: deepEquals returned false (changed)
// [MyStore] Store value changed: { previous: {...}, current: {...} }

Best Practices

✅ Do

  • Trust the system: Both Immer and comparison are needed
  • Use appropriate comparison strategies for your data types
  • Profile performance before changing default settings
  • Leverage Immer's Copy-on-Write in update functions
  • Use custom comparators for business-logic optimizations

❌ Avoid

  • Removing comparison logic "because Immer handles it"
  • Using deep comparison unnecessarily
  • Bypassing the update/setValue pipeline
  • Manually implementing immutability when Immer handles it
  • Ignoring performance characteristics of different strategies

Migration and Troubleshooting

If You're Considering Removing Comparison Logic

  1. Profile first: Measure actual performance impact
  2. Test edge cases: Identical value updates, complex objects
  3. Check React DevTools: Verify re-render behavior
  4. Benchmark: Compare with and without comparison logic

Performance Issues

If experiencing performance problems:

  1. Check comparison strategy: Use appropriate level for your data
  2. Profile comparison functions: Identify bottlenecks
  3. Consider custom comparators: Optimize for your specific use case
  4. Monitor Immer usage: Ensure efficient Copy-on-Write behavior

Integration Problems

If seeing unexpected behavior:

  1. Verify both systems are working: Enable debug mode
  2. Check custom comparators: Ensure correct logic
  3. Test update patterns: Verify Immer integration
  4. Review store configuration: Ensure proper setup

Released under the Apache-2.0 License.