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
| System | Purpose | Problem Solved | Scope |
|---|---|---|---|
| Immer | Immutability | Prevents mutation bugs, ensures safe copies | Per individual store |
| Comparison | Change Detection | Prevents unnecessary re-renders, optimizes performance | Per 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 optimizationPerformance 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 changedComparison Performance
| Strategy | Speed | Memory | Use Case |
|---|---|---|---|
| Reference | ⚡⚡⚡ | ⚡⚡⚡ | Primitives, managed objects |
| Fast Compare | ⚡⚡ | ⚡⚡ | General objects (default) |
| Shallow | ⚡⚡ | ⚡⚡ | Objects with shallow changes |
| Deep | ⚡ | ⚡ | Complex 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
Related Patterns
- Comparison Strategies - Detailed comparison options
- Memory Management - Resource-efficient patterns
- Performance Patterns - Overall optimization guide
- Store Configuration - Advanced store setup
- Memoization Patterns - Complementary optimizations
Migration and Troubleshooting
If You're Considering Removing Comparison Logic
- Profile first: Measure actual performance impact
- Test edge cases: Identical value updates, complex objects
- Check React DevTools: Verify re-render behavior
- Benchmark: Compare with and without comparison logic
Performance Issues
If experiencing performance problems:
- Check comparison strategy: Use appropriate level for your data
- Profile comparison functions: Identify bottlenecks
- Consider custom comparators: Optimize for your specific use case
- Monitor Immer usage: Ensure efficient Copy-on-Write behavior
Integration Problems
If seeing unexpected behavior:
- Verify both systems are working: Enable debug mode
- Check custom comparators: Ensure correct logic
- Test update patterns: Verify Immer integration
- Review store configuration: Ensure proper setup