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:
// "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
useSyncExternalStoreoptimization patterns for THIS store - Components subscribed to THIS store would re-render unnecessarily
✅ Correct understanding:
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:
// 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:
// "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:
// 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:
// 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:
// "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
shallowEqualoptimizes detection of changes for re-rendering- Different performance characteristics for different scenarios
✅ Understanding the roles:
// 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:
// "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:
// 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:
// "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:
// 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:
// 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:
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' });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:
// 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:
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; } }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:
// 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:
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) }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
// 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 optimizations2. Choose Comparison Strategy by Data Type
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
// 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
// Enable debugging to understand behavior
const debugStore = useStoreValue(store, undefined, {
debug: true,
debugName: 'MyStore'
});
// Monitor console for:
// - Comparison results
// - Performance metrics
// - Change detection behaviorRelated Documentation
- Immutability & Comparison Integration - Complete integration guide
- Comparison Strategies - Detailed comparison options
- Performance Patterns - Performance optimization guide
- Immutability Architecture - Technical deep-dive
- Memory Management - Resource management patterns