Automatic Batching
In a state management library, "batching" is the process of grouping multiple state updates into a single, atomic operation. This is critical for both performance and UI consistency. SoulState performs batching automatically, so you get these benefits for free.
Automatic Batching with Microtasksâ
Whenever you call store.set(), SoulState does not immediately notify its subscribers. Instead, it schedules a notification to run in a microtask.
A microtask is a short function that runs after the current JavaScript task is finished, but before the browser has a chance to repaint the screen.
import { createStore } from 'soulstate';
import { useStore } from 'soulstate/react';
// Create store with initial state
const counterStore = createStore({ count: 0 });
// Define actions that update the store
const counterActions = {
increment: () => counterStore.set(state => ({ count: state.count + 1 })),
incrementBy: (amount: number) => counterStore.set(state => ({ count: state.count + amount })),
};
function MyComponent() {
const count = useStore(counterStore, state => state.count);
const handleMultipleUpdates = () => {
// These three 'set' calls happen in the same event loop task
counterActions.increment(); // đĢ No immediate notification
counterActions.increment(); // đĢ No immediate notification
counterActions.incrementBy(5); // đĢ No immediate notification
// â
All three updates are batched into ONE notification
// Component will only re-render ONCE with count + 7
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleMultipleUpdates}>Update Multiple Times</button>
</div>
);
}
Why is this important?â
- Performance: It dramatically reduces the number of re-renders. If you have multiple state updates in a single event handler, your components will only render once with the final state.
- Prevents "Tearing": Tearing is a UI inconsistency where different components show different state values from the same update batch because they rendered at different times. By batching all notifications and rendering with the final state, SoulState ensures your entire UI is always consistent.
How Microtask Batching Worksâ
SoulState uses a simple but effective batching mechanism:
// Simplified implementation from src/core/store.ts
let isNotificationScheduled = false;
const scheduleNotification = () => {
if (!isNotificationScheduled) {
isNotificationScheduled = true;
queueMicrotask(notifySubscribers); // Batched!
}
};
const notifySubscribers = () => {
subscriptionManager.notify(state, lastKnownState);
lastKnownState = state;
isNotificationScheduled = false; // Reset for next batch
};
Update Timeline Diagramâ
Here's a visual representation of how SoulState batches updates within a single JavaScript event loop tick.
sequenceDiagram
participant User
participant Component
participant SoulState
participant MicrotaskQueue as "Microtask Queue"
participant React
User->>Component: Clicks a button
Component->>SoulState: store.set({ count: 1 })
SoulState->>MicrotaskQueue: Schedules notify()
Component->>SoulState: store.set({ count: 2 })
Component->>SoulState: store.set({ count: 3 })
Note right of SoulState: Notification already scheduled<br/>No additional scheduling
Note over Component, React: --- End of current JS task ---
MicrotaskQueue->>SoulState: Executes notify()
SoulState->>React: Triggers re-render with final state ({ count: 3 })
React->>Component: Re-renders once with final count = 3
Real-World Example: Form Handlingâ
Batching is particularly useful when handling complex form updates:
import { createStore } from 'soulstate';
import { useStore } from 'soulstate/react';
const formStore = createStore({
user: { name: '', email: '', age: 0 },
isValid: false,
isSubmitting: false
});
const formActions = {
updateField: (field: string, value: any) =>
formStore.set(state => ({
user: { ...state.user, [field]: value }
})),
validateForm: () =>
formStore.set(state => ({
isValid: state.user.name && state.user.email && state.user.age > 0
})),
startSubmission: () => formStore.set({ isSubmitting: true })
};
function UserForm() {
const { user, isValid, isSubmitting } = useStore(formStore, state => state);
const handleInputChange = (field: string, value: string) => {
// Multiple updates batched into one re-render
formActions.updateField(field, value);
formActions.validateForm();
};
const handleSubmit = async () => {
formActions.startSubmission();
// Even with async operations, related sync updates are batched
await submitToAPI(user);
formStore.set({ isSubmitting: false });
};
return (
<form>
<input
value={user.name}
onChange={(e) => handleInputChange('name', e.target.value)}
/>
{/* Only one re-render per input change, despite multiple state updates */}
</form>
);
}
Batching Across Multiple Storesâ
SoulState's batching works per-store. If you update multiple stores in the same event loop, each store will batch its own updates independently.
const storeA = createStore({ value: 0 });
const storeB = createStore({ value: 0 });
const updateBothStores = () => {
storeA.set(state => ({ value: state.value + 1 })); // Batched in storeA
storeB.set(state => ({ value: state.value + 1 })); // Batched in storeB
storeA.set(state => ({ value: state.value + 1 })); // Batched in storeA
storeB.set(state => ({ value: state.value + 1 })); // Batched in storeB
// Results in:
// - storeA: one notification with value + 2
// - storeB: one notification with value + 2
// - Components: maximum of 2 re-renders (one per store)
};
React 18+ Compatibility
SoulState's microtask batching works perfectly with React's own internal batching and the useSyncExternalStore hook, ensuring robust and predictable behavior in all versions of React 18 and beyond.
Implementation Detail
The actual batching implementation can be found in src/core/store.ts using queueMicrotask for optimal performance across all modern browsers and JavaScript environments.
Performance Benefitsâ
- Reduced Re-renders: Multiple updates = single re-render
- Better UX: No visual flickering from intermediate states
- Optimized Performance: Minimal DOM operations
- Memory Efficient: No unnecessary component instances
Key Takeaway
SoulState's automatic microtask batching gives you optimal performance by default. Write your state updates naturally, and SoulState ensures your components only re-render when necessary with the final state values.