Understanding resume() - A Beginner's Guide
Quick Start (30 seconds)
Need to re-enable reactivity after pausing it? Here's how:
const user = state({
firstName: 'John',
lastName: 'Doe'
});
effect(() => {
console.log(`${user.firstName} ${user.lastName}`);
});
// Logs: "John Doe"
// Pause reactivity
pause();
// Make changes - no effects run
user.firstName = 'Jane';
user.lastName = 'Smith';
// Resume and flush - effects run now
resume(true);
// Logs: "Jane Smith"That's it! The resume() function re-enables reactivity after pause() and optionally flushes queued effects!
What is resume()?
resume() is the counterpart to pause() that re-enables reactivity. It decrements the batch depth counter and optionally triggers all queued effects.
Resuming reactivity:
- Decrements the pause counter
- Re-enables reactivity when counter reaches 0
- Optionally flushes queued effects
- Must be paired with
pause()
Think of it as pressing the play button after pausing - reactivity starts flowing again.
Syntax
// Using the shortcut
resume(flush)
// Using the full namespace
ReactiveUtils.resume(flush)Both styles are valid! Choose whichever you prefer:
- Shortcut style (
resume()) - Clean and concise - Namespace style (
ReactiveUtils.resume()) - Explicit and clear
Parameters:
flush- Boolean indicating whether to run queued effects (optional, defaults tofalse)true- Run all queued effects immediatelyfalse- Don't run effects yet (they'll run on next change)
Returns:
- Nothing (undefined)
Why Does This Exist?
The Problem: Paused Reactivity Needs Resuming
When you pause reactivity, effects stop running:
const app = state({ count: 0 });
effect(() => {
console.log('Count:', app.count);
});
// Logs: "Count: 0"
// Pause reactivity
pause();
// Make changes
app.count = 1;
app.count = 2;
app.count = 3;
// Effects are queued but not running
// Reactivity is still paused!
// How do we trigger effects now?You need a way to re-enable reactivity and trigger queued effects.
What's the Real Issue?
After pause():
┌──────────────┐
│ Reactivity │
│ PAUSED │
└──────┬───────┘
│
▼
┌──────────────┐
│ app.count=1 │ ← Queued
└──────┬───────┘
│
▼
┌──────────────┐
│ app.count=2 │ ← Queued
└──────┬───────┘
│
▼
┌──────────────┐
│ app.count=3 │ ← Queued
└──────┬───────┘
│
▼
Effects queued
but never run!
Need resume()!Problems: ❌ Reactivity stays paused forever without resume ❌ Queued effects never run ❌ State changes but UI doesn't update ❌ No way to trigger pending effects ❌ Application appears frozen
The Solution with resume()
When you use resume(), you re-enable reactivity:
const app = state({ count: 0 });
effect(() => {
console.log('Count:', app.count);
});
// Logs: "Count: 0"
pause();
app.count = 1;
app.count = 2;
app.count = 3;
// Resume and flush
resume(true);
// Logs: "Count: 3"
// Reactivity is now active again!What Just Happened?
resume(true) Flow:
┌──────────────┐
│ resume(true) │
└──────┬───────┘
│
▼
Decrement pause
counter to 0
│
▼
Counter is 0?
Yes!
│
▼
flush = true?
Yes!
│
▼
Run all queued
effects
│
▼
✅ Reactivity
restored!With resume():
- Decrements pause counter
- When counter reaches 0, reactivity is active
- If flush=true, runs all queued effects
- Application resumes normal reactive behavior
Benefits: ✅ Re-enables reactivity ✅ Triggers queued effects (if flush=true) ✅ Completes the pause/resume cycle ✅ Flexible with flush parameter ✅ Safe with nested pauses
Mental Model
Think of resume() like pressing play on a paused video:
Paused State (Video Paused):
┌──────────────┐
│ ⏸️ Paused │
│ │
│ Frames │
│ building up │
│ but not │
│ displaying │
└──────────────┘
│
│
▼
┌──────────────┐
│ Press ▶️ │
│ resume() │
└──────┬───────┘
│
▼
┌──────────────┐
│ ▶️ Playing │
│ │
│ Show current │
│ frame │
│ │
│ Continue │
│ playing │
└──────────────┘Key Insight: Just like pressing play on a paused video resumes playback from the current position, resume() re-enables reactivity and (optionally) displays the current state by running effects.
How Does It Work?
The Magic: Decrementing Batch Depth
When you call resume(), here's what happens behind the scenes:
// What you write:
pause();
user.name = 'Jane';
resume(true);
// What actually happens (simplified):
// 1. pause() increments counter
batchDepth = 1; // pause()
// 2. State changes are queued
user.name = 'Jane'; // Queued
// 3. resume() decrements counter
batchDepth = Math.max(0, batchDepth - 1); // Now 0
// 4. If flush=true and counter=0, run effects
if (true && batchDepth === 0) {
flush(); // Run all queued effects
}In other words: resume():
- Decrements the batch depth counter
- Ensures counter doesn't go below 0
- If flush=true and counter=0, runs all queued effects
- Reactivity resumes when counter reaches 0
Under the Hood
resume(flush) implementation:
resume(flush) {
// Decrement, but don't go below 0
batchDepth = Math.max(0, batchDepth - 1);
// If flush requested and fully resumed
if (flush && batchDepth === 0) {
runAllQueuedEffects();
}
}What happens:
1️⃣ Decrements batch depth counter 2️⃣ Clamps counter to 0 (won't go negative) 3️⃣ Checks if fully resumed (counter = 0) 4️⃣ Flushes if requested and fully resumed
Basic Usage
Resume with Flush
The most common usage - resume and run effects:
const app = state({ count: 0 });
effect(() => {
console.log('Count:', app.count);
});
pause();
app.count = 5;
resume(true); // Resume and flush
// Logs: "Count: 5"Resume Without Flush
Resume but don't run effects yet:
pause();
app.count = 10;
resume(false); // Resume but don't flush
// No log yet
// Effects run on next change
app.count = 11;
// Logs: "Count: 11"Multiple Resumes for Nested Pauses
Each pause needs a corresponding resume:
pause(); // batchDepth = 1
pause(); // batchDepth = 2
app.count = 5;
resume(true); // batchDepth = 1 (still paused!)
// No log
resume(true); // batchDepth = 0 (fully resumed!)
// Logs: "Count: 5"The flush Parameter
The flush parameter controls whether to run queued effects:
flush = true (Run Effects)
pause();
app.count = 1;
app.count = 2;
app.count = 3;
resume(true); // Run effects immediately
// Logs: "Count: 3"Use when:
- You want to trigger UI updates immediately
- You're done with all state changes
- You want to see the final state reflected
flush = false (Don't Run Effects)
pause();
app.count = 1;
app.count = 2;
app.count = 3;
resume(false); // Don't run effects yet
// No log
// Effects run on next state change
app.count = 4;
// Logs: "Count: 4"Use when:
- You're not ready for effects to run
- You want effects to run on the next natural state change
- You're just cleaning up pause/resume nesting
Default Behavior
If you don't provide flush, it defaults to false:
pause();
app.count = 5;
resume(); // Same as resume(false)
// No log yet
app.count = 6;
// Logs: "Count: 6"When to Use resume()
✅ Always Use After pause()
Rule: Every pause() must have a corresponding resume():
// ✅ Correct
pause();
makeChanges();
resume(true);
// ❌ Wrong - no resume!
pause();
makeChanges();
// Reactivity stuck!✅ Use in finally Blocks
For safety, use try-finally:
pause();
try {
makeChanges();
validateChanges();
} finally {
resume(true); // Always called, even if errors
}✅ Use flush=true for Immediate Updates
function updateUI() {
pause();
app.data = newData;
app.loading = false;
resume(true); // UI updates now
}✅ Use flush=false for Deferred Updates
function cleanup() {
pause();
resetSomeState();
resume(false); // Don't update yet
}Real-World Examples
Example 1: Data Import with Progress
const app = state({
users: [],
posts: [],
comments: [],
progress: 0,
isLoading: false
});
effect(() => {
// Update progress bar
document.getElementById('progress').value = app.progress;
if (app.progress === 100) {
document.getElementById('status').textContent = 'Complete!';
}
});
async function importData(data) {
app.isLoading = true;
app.progress = 0;
pause();
try {
// Import users
app.users = data.users;
app.progress = 33;
// Import posts
app.posts = data.posts;
app.progress = 66;
// Import comments
app.comments = data.comments;
app.progress = 100;
app.isLoading = false;
} finally {
resume(true); // Show final progress
}
}Example 2: Form Reset
const form = state({
firstName: '',
lastName: '',
email: '',
errors: {},
isDirty: false
});
effect(() => {
// Update form UI
document.getElementById('firstName').value = form.firstName;
document.getElementById('lastName').value = form.lastName;
document.getElementById('email').value = form.email;
updateErrorDisplay(form.errors);
});
function resetForm() {
pause();
form.firstName = '';
form.lastName = '';
form.email = '';
form.errors = {};
form.isDirty = false;
resume(true); // Update all form fields at once
}Example 3: Multi-Step Wizard
const wizard = state({
step: 1,
data: {
step1: {},
step2: {},
step3: {}
},
isComplete: false
});
effect(() => {
renderWizardStep(wizard.step, wizard.data);
});
function completeWizard(allData) {
pause();
// Fill in all steps
wizard.data.step1 = allData.step1;
wizard.data.step2 = allData.step2;
wizard.data.step3 = allData.step3;
// Jump to completion
wizard.step = 4;
wizard.isComplete = true;
resume(true); // Show completion screen
}
function goToStep(stepNumber) {
pause();
wizard.step = stepNumber;
// Load step data if needed
if (!wizard.data[`step${stepNumber}`]) {
wizard.data[`step${stepNumber}`] = {};
}
resume(true); // Render new step
}Example 4: Batch Delete with Confirmation
const todoList = state({
todos: [
{ id: 1, text: 'Task 1', done: false },
{ id: 2, text: 'Task 2', done: true },
{ id: 3, text: 'Task 3', done: true }
]
});
effect(() => {
const count = todoList.todos.length;
const done = todoList.todos.filter(t => t.done).length;
document.getElementById('summary').textContent = `${done}/${count} done`;
});
function deleteCompleted() {
const toDelete = todoList.todos.filter(t => t.done);
if (toDelete.length === 0) {
alert('No completed tasks');
return;
}
if (!confirm(`Delete ${toDelete.length} completed tasks?`)) {
return;
}
pause();
// Delete all completed
todoList.todos = todoList.todos.filter(t => !t.done);
resume(true); // Update UI after all deletions
alert(`Deleted ${toDelete.length} tasks`);
}Common Patterns
Pattern: Safe Resume with try-finally
function safeUpdate() {
pause();
try {
makeComplexUpdates();
} catch (error) {
console.error('Update failed:', error);
} finally {
resume(true); // Always resume
}
}Pattern: Conditional Flush
function updateWithValidation(data) {
pause();
Object.assign(state, data);
const isValid = validate(state);
if (isValid) {
resume(true); // Flush if valid
} else {
resume(false); // Don't flush if invalid
state.errors = getErrors(state);
}
}Pattern: Nested Pause/Resume
function complexUpdate() {
pause();
updateSection1();
pause(); // Nested
updateSection2();
updateSection3();
resume(false); // Inner resume (no flush)
updateSection4();
resume(true); // Outer resume (flush)
}Pattern: Progress Updates During Long Operations
async function processItems(items) {
pause();
for (let i = 0; i < items.length; i++) {
await processItem(items[i]);
app.progress = ((i + 1) / items.length) * 100;
// Temporarily resume to show progress
resume(true);
pause();
await sleep(100); // Smooth progress updates
}
resume(true); // Final resume
}Common Pitfalls
Pitfall #1: Forgetting to Resume
❌ Wrong:
pause();
user.name = 'John';
// Forgot resume!
// Reactivity is stuck!
user.age = 30; // Effect won't run✅ Correct:
pause();
try {
user.name = 'John';
} finally {
resume(true); // Always resume
}Pitfall #2: Mismatched Pause/Resume Counts
❌ Wrong:
pause();
pause();
user.name = 'John';
resume(true); // Only 1 resume, still paused!
user.age = 30; // Won't trigger effects✅ Correct:
pause();
pause();
user.name = 'John';
resume(true);
resume(true); // Match pause count
user.age = 30; // Will trigger effectsPitfall #3: Expecting flush=false to Undo Changes
❌ Wrong:
pause();
user.name = 'John';
resume(false); // Doesn't undo the change!
console.log(user.name); // Still "John"✅ Correct:
pause();
const original = { ...user };
user.name = 'John';
if (!isValid(user)) {
Object.assign(user, original); // Manually revert
}
resume(false);Why? resume(false) just doesn't flush - it doesn't revert changes.
Pitfall #4: Calling resume() Without pause()
❌ Wrong:
// No pause() call
user.name = 'John';
resume(true); // Might decrement counter unnecessarily✅ Correct:
pause();
user.name = 'John';
resume(true); // Properly pairedWhy? resume() decrements a counter. Calling it without pause() can cause issues if the counter goes to 0 when it shouldn't.
Summary
What is resume()?
resume() is the counterpart to pause() that re-enables reactivity by decrementing the batch depth counter and optionally flushing queued effects.
Why use resume()?
- Required to complete pause/resume cycle
- Re-enables reactivity after pausing
- Triggers queued effects (if flush=true)
- Allows manual control over effect timing
- Safe with nested pauses
Key Points to Remember:
1️⃣ Always pair with pause() - Every pause() needs a resume() 2️⃣ flush parameter - true runs effects, false defers them 3️⃣ Use try-finally - Ensures resume is always called 4️⃣ Nested pauses - Each pause needs a matching resume 5️⃣ Safe counter - Won't go below 0
Mental Model: Think of resume() as pressing play on a paused video - it resumes the flow and (optionally) displays the current frame.
Quick Reference:
// Basic usage
pause();
user.name = 'John';
resume(true); // Resume and flush
// Safe with try-finally
pause();
try {
user.name = 'John';
} finally {
resume(true); // Always called
}
// Nested pauses
pause();
pause();
user.name = 'John';
resume(true);
resume(true); // Match pause count
// Conditional flush
pause();
user.name = 'John';
if (isValid(user)) {
resume(true); // Flush if valid
} else {
resume(false); // Don't flush if invalid
}Remember: resume() is the essential partner to pause(). Always use it to re-enable reactivity, and use the flush parameter to control when effects run!