Skip to content

Immer and Comparison Misconceptions

Common misconceptions about Immer and comparison logic integration in Context-Action stores, with clear explanations and solutions.

Overview

This guide addresses the most common misconceptions developers have when working with Context-Action's dual-layer optimization system (Immer + Comparison Logic).

Common Misconceptions

❌ Misconception #1: "Immer replaces comparison logic"

Wrong assumption:

typescript
// "Since we use Immer, comparison logic is redundant"
setValue(newValue) {
  this._value = produce(newValue, draft => {});
  this._notifyListeners(); // Always notify! - WRONG: affects this store's subscribers unnecessarily
}

Why it's wrong:

  • Would trigger React re-render even when THIS store's value hasn't changed
  • Breaks performance optimization for identical values in THIS store
  • Ignores React's useSyncExternalStore optimization patterns for THIS store
  • Components subscribed to THIS store would re-render unnecessarily

✅ Correct understanding:

typescript
setValue(newValue) {
  // Layer 1: Immer ensures safe immutability for THIS store's value
  const safeValue = produce(newValue, draft => {});
  
  // Layer 2: Comparison prevents unnecessary re-renders for THIS store
  if (this._compareValues(this._value, safeValue)) {
    this._value = safeValue;
    this._notifyListeners(); // Only when THIS store actually changed
  }
}

Real-world impact:

typescript
// Without comparison - always re-renders when userStore is updated
const UserProfile = () => {
  const user = useStoreValue(userStore);  // Subscribed to userStore
  console.log('Rendered'); // Logs every time userStore is updated, even for identical data
  return <div>{user.name}</div>;
};

// With comparison - optimized re-renders for userStore
const OptimizedUserProfile = () => {
  const user = useStoreValue(userStore);  // Subscribed to userStore
  console.log('Rendered'); // Only logs when userStore data actually changes
  return <div>{user.name}</div>;
};

// Important: Other stores (cartStore, settingsStore, etc.) are unaffected
// Each store operates independently with its own Immer + Comparison system

❌ Misconception #2: "Immer's same reference check eliminates need for comparison"

Wrong assumption:

typescript
// "Immer returns same reference if unchanged, so no comparison needed"
update(updater) {
  const result = produce(currentValue, updater);
  if (result === currentValue) return; // This only works sometimes!
  
  this._value = result;
  this._notifyListeners();
}

Why it's partially wrong:

  • Only works for update() method with no-op updater functions
  • Doesn't work for setValue() with identical content but different references
  • Misses business logic optimization opportunities

✅ Complete solution:

typescript
// Handles all cases correctly
setValue(newValue) {
  const safeValue = deepClone(newValue);
  
  // Comparison handles:
  // 1. Identical references (Immer optimization case)
  // 2. Identical content, different references
  // 3. Custom business logic comparisons
  if (this._compareValues(this._value, safeValue)) {
    this._value = safeValue;
    this._notifyListeners();
  }
}

Examples where reference check fails:

typescript
// Case 1: setValue with same content, different reference
store.setValue({ name: 'John', age: 30 }); // First call
store.setValue({ name: 'John', age: 30 }); // Same content, different object
// Without comparison: Would re-render unnecessarily

// Case 2: API data updates
const apiData = await fetchUser(id);
store.setValue(apiData); // Might be identical to current data
// Without comparison: Would re-render even if data unchanged

❌ Misconception #3: "shallowEqual is obsolete with Immer"

Wrong assumption:

typescript
// "Immer handles object mutations, so shallow comparison isn't needed"
const Component = () => {
  // Assuming this is automatically optimized
  const user = useStoreValue(userStore);
  return <div>{user.profile.name}</div>;
};

Why it's wrong:

  • Immer handles creation of immutable updates
  • shallowEqual optimizes detection of changes for re-rendering
  • Different performance characteristics for different scenarios

✅ Understanding the roles:

typescript
// Immer: Safe update creation
const updateUser = (newData) => {
  userStore.update(current => ({
    ...current,
    profile: { ...current.profile, ...newData }
  }));
  // Immer ensures this mutation-like syntax creates immutable result
};

// shallowEqual: Optimized change detection
const userProfile = useStoreValue(userStore, user => user.profile, {
  customComparator: (prev, next) => shallowEqual(prev, next)
});
// Only re-renders when profile properties actually change

❌ Misconception #4: "Deep comparison is always slow and should be avoided"

Wrong assumption:

typescript
// "Never use deep comparison because it's slow"
const config = {
  strategy: 'reference' // Always use fastest
};

Why it's too simplistic:

  • Modern deep comparison is optimized (early exits, caching)
  • Sometimes deep comparison prevents expensive re-computations
  • Business logic may require deep equality

✅ Nuanced approach:

typescript
// Use appropriate strategy for your data
const strategies = {
  // Fast changing primitives
  counter: { strategy: 'reference' },
  
  // UI state objects
  settings: { strategy: 'shallow' },
  
  // Complex nested data where deep changes matter
  formData: { 
    strategy: 'deep',
    // Or custom for business logic optimization
    customComparator: (prev, next) => {
      // Only compare fields that affect validation
      return prev.email === next.email && prev.isValid === next.isValid;
    }
  }
};

❌ Misconception #5: "Performance optimizations are premature optimization"

Wrong assumption:

typescript
// "Don't worry about comparison strategies until there's a problem"
const store = createStore({ /* all defaults */ });

Why it's problematic:

  • React re-renders compound exponentially in large apps
  • Comparison strategies are architectural decisions, not micro-optimizations
  • Easier to set up correctly from the start than to optimize later

✅ Proactive approach:

typescript
// Design with performance in mind from the start
const { Provider, useStore } = createStoreContext('App', {
  // Primitive values - reference comparison (default)
  isLoading: false,
  
  // Simple objects - shallow comparison
  user: { 
    initialValue: { name: '', role: '' },
    strategy: 'shallow'
  },
  
  // Complex data - custom optimization
  dashboard: {
    initialValue: { /* complex data */ },
    comparisonOptions: {
      strategy: 'custom',
      customComparator: (prev, next) => {
        // Only update if timestamp changed (data versioning)
        return prev.lastModified === next.lastModified;
      }
    }
  }
});

Troubleshooting Guide

Issue: Unexpected Re-renders

Symptoms:

  • Components re-render even when data hasn't changed
  • Performance issues in large applications
  • DevTools showing frequent updates

Diagnosis:

typescript
// Add debugging to identify the issue
const userData = useStoreValue(userStore, undefined, {
  debug: true,
  debugName: 'UserData'
});

// Console will show:
// [UserData] Store value changed: false (no actual change detected)
// vs
// [UserData] Store value changed: true (change detected)

Solutions:

  1. Check comparison strategy:

    typescript
    // If using reference comparison for objects
    const user = useStoreValue(userStore); // May re-render unnecessarily
    
    // Switch to appropriate strategy
    userStore.setComparisonOptions({ strategy: 'shallow' });
  2. Verify data source:

    typescript
    // API calls creating new objects
    const fetchUser = async () => {
      const data = await api.getUser();
      // This creates new object every time, even if data identical
      userStore.setValue(data);
    };
    
    // Add comparison at API level or use appropriate store strategy

Issue: Performance Problems

Symptoms:

  • Slow updates or laggy UI
  • High CPU usage during state updates
  • Memory usage increasing over time

Diagnosis:

typescript
// Profile comparison performance manually
import { deepEquals } from '@context-action/react';

const startTime = performance.now();
const result = deepEquals(oldValue, newValue);
const executionTime = performance.now() - startTime;

console.log('Comparison took:', executionTime, 'ms');

Solutions:

  1. Optimize comparison strategy:

    typescript
    // From slow deep comparison
    { strategy: 'deep' }
    
    // To optimized custom comparison
    {
      strategy: 'custom',
      customComparator: (prev, next) => {
        // Only compare fields that matter for your use case
        return prev.id === next.id && prev.version === next.version;
      }
    }
  2. Use Immer optimization:

    typescript
    // Leverage Immer's Copy-on-Write
    userStore.update(current => {
      if (current.status === targetStatus) {
        return current; // Immer returns same reference
      }
      return { ...current, status: targetStatus };
    });

Issue: Incorrect Behavior

Symptoms:

  • Changes not triggering updates when they should
  • Updates triggering when they shouldn't
  • Inconsistent behavior across different data types

Diagnosis:

typescript
// Check comparison behavior
const testComparison = () => {
  const oldVal = { name: 'John', nested: { age: 30 } };
  const newVal = { name: 'John', nested: { age: 30 } };
  
  console.log('Reference:', Object.is(oldVal, newVal)); // false
  console.log('Shallow:', shallowEqual(oldVal, newVal)); // false (different nested ref)
  console.log('Deep:', deepEqual(oldVal, newVal)); // true
  console.log('JSON:', JSON.stringify(oldVal) === JSON.stringify(newVal)); // true
};

Solutions:

  1. Match strategy to data structure:

    typescript
    // For nested objects that should be considered equal by content
    { strategy: 'deep' }
    // or
    { 
      strategy: 'custom',
      customComparator: (a, b) => JSON.stringify(a) === JSON.stringify(b)
    }
  2. Handle edge cases:

    typescript
    {
      strategy: 'custom',
      customComparator: (prev, next) => {
        // Handle null/undefined
        if (prev == null && next == null) return true;
        if (prev == null || next == null) return false;
        
        // Custom business logic
        return prev.id === next.id && prev.hash === next.hash;
      }
    }

Best Practices for Avoiding Misconceptions

1. Understand the Dual-Layer System

typescript
// Think of it as two separate, complementary systems:

// Layer 1: Immutability (Immer)
// - Prevents bugs from accidental mutations
// - Enables safe update patterns
// - Optimizes object creation with Copy-on-Write

// Layer 2: Change Detection (Comparison)
// - Prevents unnecessary re-renders
// - Optimizes React integration
// - Enables business logic optimizations

2. Choose Comparison Strategy by Data Type

typescript
const strategyGuide = {
  // Primitives and managed object references
  primitives: { strategy: 'reference' },
  
  // Simple objects with shallow property changes
  simpleObjects: { strategy: 'shallow' },
  
  // Complex nested structures where deep equality matters
  nestedObjects: { strategy: 'deep' },
  
  // Business logic specific comparisons
  customLogic: { 
    strategy: 'custom',
    customComparator: (a, b) => {/* business logic */}
  }
};

3. Profile Before Optimizing

typescript
// Always measure before changing strategy
const profileStorePerformance = () => {
  const strategies = {
    reference: (a, b) => Object.is(a, b),
    shallow: (a, b) => shallowEquals(a, b),
    deep: (a, b) => deepEquals(a, b)
  };

  Object.entries(strategies).forEach(([name, fn]) => {
    const start = performance.now();
    const result = fn(oldValue, newValue);
    const time = performance.now() - start;
    console.log(`${name}:`, time.toFixed(2), 'ms, result:',  result);
  });
};

4. Use Debug Mode During Development

typescript
// Enable debugging to understand behavior
const debugStore = useStoreValue(store, undefined, {
  debug: true,
  debugName: 'MyStore'
});

// Monitor console for:
// - Comparison results
// - Performance metrics
// - Change detection behavior

Released under the Apache-2.0 License.