Understanding batch() - A Beginner's Guide
Quick Start (30 seconds)
Need to update multiple reactive properties without triggering effects repeatedly? Here's how:
const user = state({
firstName: 'John',
lastName: 'Doe',
age: 25
});
effect(() => {
console.log(`${user.firstName} ${user.lastName}, age ${user.age}`);
});
// Logs: "John Doe, age 25"
// ❌ Without batch - effect runs 3 times
user.firstName = 'Jane'; // Logs: "Jane Doe, age 25"
user.lastName = 'Smith'; // Logs: "Jane Smith, age 25"
user.age = 30; // Logs: "Jane Smith, age 30"
// ✅ With batch - effect runs once
batch(() => {
user.firstName = 'Bob';
user.lastName = 'Johnson';
user.age = 35;
});
// Logs only once: "Bob Johnson, age 35"That's it! The batch() function groups multiple state updates together so effects only run once at the end!
What is batch()?
batch() is a performance optimization function that groups multiple state updates together. Instead of triggering effects, watchers, and computed properties after each individual change, it waits until all changes are complete and then triggers them once.
Batching:
- Defers effect execution during updates
- Runs all effects once after all changes complete
- Prevents unnecessary re-renders and re-computations
- Improves performance for bulk updates
Think of it as bundling multiple notifications into one - instead of sending an alert for each change, it sends one alert after all changes are done.
Syntax
// Using the shortcut
batch(fn)
// Using the full namespace
ReactiveUtils.batch(fn)Both styles are valid! Choose whichever you prefer:
- Shortcut style (
batch()) - Clean and concise - Namespace style (
ReactiveUtils.batch()) - Explicit and clear
Parameters:
fn- A function that performs multiple state updates (required)
Returns:
- The return value of the function
fn
Why Does This Exist?
The Problem with Multiple State Updates
Let's say you need to update several reactive properties:
// Create reactive state with an effect
const user = state({
firstName: 'John',
lastName: 'Doe',
age: 25,
city: 'New York'
});
effect(() => {
console.log(`${user.firstName} ${user.lastName}, age ${user.age}, lives in ${user.city}`);
// Imagine this is an expensive DOM update or API call
});
// Logs: "John Doe, age 25, lives in New York"
// Update multiple properties
user.firstName = 'Jane'; // Effect runs!
user.lastName = 'Smith'; // Effect runs!
user.age = 30; // Effect runs!
user.city = 'Boston'; // Effect runs!This works, but it's inefficient. The effect runs 4 times - once for each property change!
What's the Real Issue?
Normal Updates Flow:
┌─────────────────┐
│ user.firstName │
│ = 'Jane' │
└────────┬────────┘
│
▼
Effect runs! 📢
│
▼
┌─────────────────┐
│ user.lastName │
│ = 'Smith' │
└────────┬────────┘
│
▼
Effect runs! 📢
│
▼
┌─────────────────┐
│ user.age = 30 │
└────────┬────────┘
│
▼
Effect runs! 📢
│
▼
┌─────────────────┐
│ user.city │
│ = 'Boston' │
└────────┬────────┘
│
▼
Effect runs! 📢
│
▼
4 executions!
Wasteful!Problems: ❌ Effect runs multiple times for related changes ❌ Expensive operations (DOM updates, calculations) happen repeatedly ❌ Intermediate states are visible (firstName changes but lastName hasn't yet) ❌ Performance degrades with many updates ❌ UI flickers during multi-step updates ❌ Wasted computation and resources
Why This Becomes a Problem:
Imagine the effect does something expensive:
- Updates multiple DOM elements
- Makes API calls
- Performs complex calculations
- Triggers animations
Running it 4 times when you only need it once is wasteful!
The Solution with batch()
When you use batch(), all updates are grouped together:
// Same state and effect setup
const user = state({
firstName: 'John',
lastName: 'Doe',
age: 25,
city: 'New York'
});
effect(() => {
console.log(`${user.firstName} ${user.lastName}, age ${user.age}, lives in ${user.city}`);
});
// Logs: "John Doe, age 25, lives in New York"
// Update multiple properties in a batch
batch(() => {
user.firstName = 'Jane';
user.lastName = 'Smith';
user.age = 30;
user.city = 'Boston';
});
// Logs only once: "Jane Smith, age 30, lives in Boston"What Just Happened?
Batched Updates Flow:
┌──────────────────┐
│ batch(() => { │
└────────┬─────────┘
│
▼
┌─────────────────┐
│ firstName='Jane'│ ← Change tracked
└────────┬────────┘
│
▼
┌─────────────────┐
│lastName='Smith' │ ← Change tracked
└────────┬────────┘
│
▼
┌─────────────────┐
│ age = 30 │ ← Change tracked
└────────┬────────┘
│
▼
┌─────────────────┐
│ city = 'Boston' │ ← Change tracked
└────────┬────────┘
│
▼
┌─────────────────┐
│ }) │
└────────┬────────┘
│
▼
Effect runs
ONCE! 🎯With batch():
- All updates happen inside the function
- Effects are deferred until the function completes
- Effects run once with the final state
- No intermediate states visible
- Much better performance
Benefits: ✅ Effects run once instead of multiple times ✅ Better performance for bulk updates ✅ No intermediate states visible ✅ Cleaner, more predictable behavior ✅ Prevents UI flicker ✅ Reduces wasted computation
Mental Model
Think of batch() like sending a group email:
Normal Updates (Individual Emails):
┌──────────────┐
│ Send email 1 │ → Recipient gets notification
└──────────────┘
┌──────────────┐
│ Send email 2 │ → Recipient gets notification
└──────────────┘
┌──────────────┐
│ Send email 3 │ → Recipient gets notification
└──────────────┘
┌──────────────┐
│ Send email 4 │ → Recipient gets notification
└──────────────┘
4 notifications!
Annoying!
Batched Updates (Group Email):
┌──────────────────────┐
│ Draft email 1 │
│ Draft email 2 │
│ Draft email 3 │
│ Draft email 4 │
│ │
│ Send all at once │
└──────────┬───────────┘
│
▼
1 notification!
Efficient!Key Insight: Just like drafting multiple emails and sending them all at once creates one notification instead of many, batch() groups multiple state changes and triggers effects once instead of repeatedly.
How Does It Work?
The Magic: Batching Depth Counter
When you call batch(), here's what happens behind the scenes:
// What you write:
batch(() => {
user.firstName = 'Jane';
user.lastName = 'Smith';
user.age = 30;
});
// What actually happens (simplified):
// 1. Increment batch depth counter
batchDepth = 1;
// 2. Run your function
user.firstName = 'Jane'; // Effect queued, not run
user.lastName = 'Smith'; // Effect queued, not run
user.age = 30; // Effect queued, not run
// 3. Decrement batch depth counter
batchDepth = 0;
// 4. Flush all queued effects (batch depth is now 0)
runAllQueuedEffects(); // Effects run now!In other words: batch() uses a counter:
- Increments counter before running your function
- While counter > 0, effects are queued instead of run
- Decrements counter after your function completes
- When counter reaches 0, all queued effects run at once
Under the Hood
Before batch():
batchDepth = 0
┌──────────────┐
│ State change │
└──────┬───────┘
│
▼
Run effect
immediately
Inside batch():
batchDepth = 1
┌──────────────┐
│ State change │
└──────┬───────┘
│
▼
Queue effect
(don't run yet)
After batch():
batchDepth = 0
┌──────────────┐
│ Flush queue │
└──────┬───────┘
│
▼
Run all queued
effects onceWhat happens:
1️⃣ batch() increments the batch depth counter 2️⃣ Inside batch(), state changes queue effects instead of running them 3️⃣ batch() decrements the counter when done 4️⃣ When counter reaches 0, all queued effects run once
Basic Usage
Batching Multiple Updates
The simplest way to use batch():
const app = state({
count: 0,
multiplier: 1,
offset: 0
});
effect(() => {
console.log('Result:', app.count * app.multiplier + app.offset);
});
// Logs: "Result: 0"
// Without batch - effect runs 3 times
app.count = 10; // Logs: "Result: 10"
app.multiplier = 2; // Logs: "Result: 20"
app.offset = 5; // Logs: "Result: 25"
// With batch - effect runs once
batch(() => {
app.count = 100;
app.multiplier = 3;
app.offset = 10;
});
// Logs only once: "Result: 310"Returning Values from batch()
batch() returns whatever your function returns:
const user = state({ score: 0, bonus: 0 });
const total = batch(() => {
user.score = 100;
user.bonus = 50;
return user.score + user.bonus;
});
console.log(total); // 150Nested Batching
Batches can be nested:
const app = state({ a: 0, b: 0, c: 0 });
effect(() => {
console.log(`a=${app.a}, b=${app.b}, c=${app.c}`);
});
batch(() => {
app.a = 1;
batch(() => {
app.b = 2;
app.c = 3;
}); // Inner batch completes, but effects still queued
app.a = 10;
}); // Outer batch completes, effects run once
// Logs: "a=10, b=2, c=3"Performance Impact
Measuring the Difference
Let's compare performance:
const items = state({ list: [] });
effect(() => {
// Expensive operation: update all DOM elements
document.getElementById('count').textContent = items.list.length;
items.list.forEach((item, i) => {
const el = document.getElementById(`item-${i}`);
if (el) el.textContent = item;
});
});
// ❌ Without batch - effect runs 100 times
console.time('without batch');
for (let i = 0; i < 100; i++) {
items.list.push(`Item ${i}`);
}
console.timeEnd('without batch');
// Output: "without batch: 45ms"
items.list = []; // Reset
// ✅ With batch - effect runs once
console.time('with batch');
batch(() => {
for (let i = 0; i < 100; i++) {
items.list.push(`Item ${i}`);
}
});
console.timeEnd('with batch');
// Output: "with batch: 2ms"Performance gain: 20x faster!
When to Use batch()
✅ Good Use Cases
1. Bulk Updates
batch(() => {
todos.items.forEach(todo => {
todo.completed = false;
});
});2. Form Resets
batch(() => {
loginForm.email = '';
loginForm.password = '';
loginForm.errors = {};
loginForm.touched = {};
});3. Data Initialization
batch(() => {
app.user = userData;
app.preferences = preferencesData;
app.settings = settingsData;
app.isLoaded = true;
});4. Multi-Step Calculations
batch(() => {
cart.subtotal = calculateSubtotal();
cart.tax = calculateTax(cart.subtotal);
cart.shipping = calculateShipping();
cart.total = cart.subtotal + cart.tax + cart.shipping;
});❌ Not Needed
1. Single Updates
// Don't use batch for single updates
❌ batch(() => {
user.name = 'John';
});
// Just update directly
✅ user.name = 'John';2. Already Batched
Many operations are already batched internally:
// These are often already optimized
userset({ name: 'John', age: 30 }); // Already batched internallyReal-World Examples
Example 1: Todo List Bulk Actions
const todoApp = state({
todos: [
{ id: 1, text: 'Task 1', done: false },
{ id: 2, text: 'Task 2', done: false },
{ id: 3, text: 'Task 3', done: false }
]
});
effect(() => {
const completed = todoApp.todos.filter(t => t.done).length;
const total = todoApp.todos.length;
console.log(`Progress: ${completed}/${total}`);
document.getElementById('progress').textContent = `${completed}/${total}`;
});
// Mark all as complete - single effect run
function completeAll() {
batch(() => {
todoApp.todos.forEach(todo => {
todo.done = true;
});
});
}
completeAll();
// Logs once: "Progress: 3/3"Example 2: Shopping Cart Calculations
const cart = state({
items: [],
subtotal: 0,
tax: 0,
shipping: 0,
total: 0
});
effect(() => {
// Expensive: Update multiple DOM elements
document.getElementById('subtotal').textContent = `$${cart.subtotal}`;
document.getElementById('tax').textContent = `$${cart.tax}`;
document.getElementById('shipping').textContent = `$${cart.shipping}`;
document.getElementById('total').textContent = `$${cart.total}`;
});
function addToCart(product, quantity) {
batch(() => {
// Add item
cart.items.push({ product, quantity, price: product.price * quantity });
// Recalculate everything
cart.subtotal = cart.items.reduce((sum, item) => sum + item.price, 0);
cart.tax = cart.subtotal * 0.08;
cart.shipping = cart.subtotal > 50 ? 0 : 5.99;
cart.total = cart.subtotal + cart.tax + cart.shipping;
});
// Effect runs once with all updates
}
addToCart({ name: 'Book', price: 29.99 }, 2);Example 3: Animation Frame Updates
const animation = state({
x: 0,
y: 0,
rotation: 0,
scale: 1
});
effect(() => {
const element = document.getElementById('animated');
element.style.transform =
`translate(${animation.x}px, ${animation.y}px) ` +
`rotate(${animation.rotation}deg) ` +
`scale(${animation.scale})`;
});
// Animation loop - batch all updates per frame
function animate() {
batch(() => {
animation.x += 1;
animation.y += 0.5;
animation.rotation += 2;
animation.scale = 1 + Math.sin(Date.now() / 1000) * 0.1;
});
// Single DOM update per frame
requestAnimationFrame(animate);
}
animate();Example 4: Form Validation and Update
const registrationForm = state({
email: '',
password: '',
confirmPassword: '',
errors: {}
});
effect(() => {
// Update all error messages in UI
Object.entries(registrationForm.errors).forEach(([field, error]) => {
const el = document.getElementById(`error-${field}`);
if (el) el.textContent = error;
});
});
function validateAndUpdate(formData) {
batch(() => {
// Update all fields
registrationForm.email = formData.email;
registrationForm.password = formData.password;
registrationForm.confirmPassword = formData.confirmPassword;
// Validate all
const errors = {};
if (!formData.email.includes('@')) {
errors.email = 'Invalid email';
}
if (formData.password.length < 6) {
errors.password = 'Too short';
}
if (formData.password !== formData.confirmPassword) {
errors.confirmPassword = 'Passwords must match';
}
registrationForm.errors = errors;
});
// UI updates once with all errors
}Common Patterns
Pattern: Batch with Conditional Logic
function updateUserProfile(updates) {
batch(() => {
if (updates.name) user.name = updates.name;
if (updates.email) user.email = updates.email;
if (updates.age) user.age = updates.age;
// Always update timestamp
user.lastModified = Date.now();
});
}Pattern: Batch with Error Handling
function safeUpdate(updates) {
try {
batch(() => {
Object.assign(state, updates);
});
} catch (error) {
console.error('Update failed:', error);
// State is still consistent (atomic update)
}
}Pattern: Batch with Return Value
function calculateAndUpdate(values) {
return batch(() => {
calculator.input1 = values[0];
calculator.input2 = values[1];
calculator.operation = values[2];
const result = calculator.input1 + calculator.input2;
calculator.result = result;
return result;
});
}
const sum = calculateAndUpdate([10, 20, '+']);
console.log(sum); // 30Pattern: Nested Batches for Complex Updates
function importData(data) {
batch(() => {
// Phase 1: Clear old data
batch(() => {
app.users = [];
app.posts = [];
app.comments = [];
});
// Phase 2: Import new data
batch(() => {
app.users = data.users;
app.posts = data.posts;
app.comments = data.comments;
});
// Phase 3: Update metadata
app.lastImport = Date.now();
app.importedCount = data.users.length;
});
}Common Pitfalls
Pitfall #1: Async Functions in batch()
❌ Wrong:
// Async functions don't work as expected
batch(async () => {
user.name = 'Loading...';
const data = await fetchUser(); // batch() returns here!
user.name = data.name; // This runs OUTSIDE the batch!
});✅ Correct:
// Separate the async parts
const data = await fetchUser();
batch(() => {
user.name = data.name;
user.email = data.email;
user.age = data.age;
});Why? batch() completes as soon as your function returns, which happens immediately with async functions (they return a Promise).
Pitfall #2: Forgetting to Return Values
❌ Wrong:
const result = batch(() => {
calculate.a = 10;
calculate.b = 20;
calculate.a + calculate.b; // Forgot to return!
});
console.log(result); // undefined✅ Correct:
const result = batch(() => {
calculate.a = 10;
calculate.b = 20;
return calculate.a + calculate.b;
});
console.log(result); // 30Pitfall #3: Over-Batching Simple Updates
❌ Wrong:
// Don't batch single updates
batch(() => {
user.name = 'John';
});✅ Correct:
// Just update directly
user.name = 'John';Why? Batching has a tiny overhead. For single updates, it's unnecessary.
Pitfall #4: Expecting Immediate Effect Updates Inside Batch
❌ Wrong:
const log = [];
effect(() => {
log.push(counter.count);
});
batch(() => {
counter.count = 1;
console.log(log); // Still [0] - effect hasn't run yet!
counter.count = 2;
});
// Effect runs here, log becomes [0, 2]✅ Correct:
batch(() => {
counter.count = 1;
counter.count = 2;
});
// Effect runs here
console.log(log); // [0, 2]Why? Effects don't run until the batch completes.
Summary
What is batch()?
batch() is a performance optimization function that groups multiple state updates together so effects only run once at the end instead of after each change.
Why use batch() instead of individual updates?
- Better performance for bulk updates
- Effects run once instead of multiple times
- No intermediate states visible
- Prevents UI flicker
- Reduces wasted computation
- More predictable behavior
Key Points to Remember:
1️⃣ Groups updates - Multiple changes trigger effects once 2️⃣ Performance boost - Especially important for expensive effects 3️⃣ Returns value - Can return a value from the batch function 4️⃣ Nestable - Batches can be nested safely 5️⃣ Synchronous only - Don't use with async functions
Mental Model: Think of batch() as sending a group email - instead of sending individual notifications for each change, it bundles them all into one notification.
Quick Reference:
// Without batch - effect runs 3 times
user.firstName = 'Jane';
user.lastName = 'Smith';
user.age = 30;
// With batch - effect runs once
batch(() => {
user.firstName = 'Jane';
user.lastName = 'Smith';
user.age = 30;
});
// With return value
const total = batch(() => {
cart.subtotal = 100;
cart.tax = 8;
cart.shipping = 5;
return cart.subtotal + cart.tax + cart.shipping;
});
console.log(total); // 113Remember: batch() is your performance optimization tool for bulk updates. Use it whenever you need to update multiple reactive properties and want effects to run only once at the end!