Skip to content

Sponsor

Sponsor

Understanding resume() - A Beginner's Guide

Quick Start (30 seconds)

Need to re-enable reactivity after pausing it? Here's how:

js
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

js
// 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 to false)
    • true - Run all queued effects immediately
    • false - 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:

javascript
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:

javascript
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:

javascript
// 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():

  1. Decrements the batch depth counter
  2. Ensures counter doesn't go below 0
  3. If flush=true and counter=0, runs all queued effects
  4. 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:

js
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:

js
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:

js
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)

js
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)

js
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:

js
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():

js
// ✅ Correct
pause();
makeChanges();
resume(true);

// ❌ Wrong - no resume!
pause();
makeChanges();
// Reactivity stuck!

✅ Use in finally Blocks

For safety, use try-finally:

js
pause();
try {
  makeChanges();
  validateChanges();
} finally {
  resume(true);  // Always called, even if errors
}

✅ Use flush=true for Immediate Updates

js
function updateUI() {
  pause();
  app.data = newData;
  app.loading = false;
  resume(true);  // UI updates now
}

✅ Use flush=false for Deferred Updates

js
function cleanup() {
  pause();
  resetSomeState();
  resume(false);  // Don't update yet
}

Real-World Examples

Example 1: Data Import with Progress

js
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

js
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

js
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

js
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

js
function safeUpdate() {
  pause();
  try {
    makeComplexUpdates();
  } catch (error) {
    console.error('Update failed:', error);
  } finally {
    resume(true);  // Always resume
  }
}

Pattern: Conditional Flush

js
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

js
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

js
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:

js
pause();
user.name = 'John';
// Forgot resume!

// Reactivity is stuck!
user.age = 30;  // Effect won't run

Correct:

js
pause();
try {
  user.name = 'John';
} finally {
  resume(true);  // Always resume
}

Pitfall #2: Mismatched Pause/Resume Counts

Wrong:

js
pause();
pause();
user.name = 'John';
resume(true);  // Only 1 resume, still paused!

user.age = 30;  // Won't trigger effects

Correct:

js
pause();
pause();
user.name = 'John';
resume(true);
resume(true);  // Match pause count

user.age = 30;  // Will trigger effects

Pitfall #3: Expecting flush=false to Undo Changes

Wrong:

js
pause();
user.name = 'John';
resume(false);  // Doesn't undo the change!

console.log(user.name);  // Still "John"

Correct:

js
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:

js
// No pause() call
user.name = 'John';
resume(true);  // Might decrement counter unnecessarily

Correct:

js
pause();
user.name = 'John';
resume(true);  // Properly paired

Why? 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:

js
// 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!