collector.cleanup()
Execute all cleanup functions stored in a collector and mark it as disposed.
Quick Start (30 seconds)
// Create collector and add cleanups
const myCollector = collector();
myCollector.add(() => console.log('Cleanup 1'));
myCollector.add(() => console.log('Cleanup 2'));
myCollector.add(() => console.log('Cleanup 3'));
// Execute all cleanups
myCollector.cleanup();
// Logs: Cleanup 1, Cleanup 2, Cleanup 3
// Collector is now disposed
console.log(myCollector.disposed); // true
console.log(myCollector.size); // 0The magic: cleanup() runs all cleanup functions you've added and marks the collector as disposed—one call to clean everything!
What is collector.cleanup()?
collector.cleanup() is an instance method on a collector object that executes all registered cleanup functions in the order they were added, then marks the collector as disposed.
Simply put: It's the "run everything and shut down" button for your collector.
Think of it like this:
- You've added cleanup functions throughout your code
- They're sitting in the collector, waiting
- You call
cleanup()when you're done - All cleanups run, resources are freed, and the collector shuts down
Syntax
// Create collector and add cleanups
const myCollector = collector();
myCollector.add(cleanup1);
myCollector.add(cleanup2);
// Execute all cleanups
myCollector.cleanup();Parameters:
- None—
cleanup()takes no parameters
Returns:
undefined(returns nothing)
Side Effects:
- Executes all cleanup functions in order
- Sets the
disposedflag totrue - Clears the internal cleanup array (sets
sizeto 0) - Further
add()calls will be ignored
Why Does This Exist?
The Challenge with Manual Cleanup
Imagine you've created multiple features, each with its own cleanup:
const state = ReactiveUtils.state({ count: 0 });
// Feature 1
const unwatch1 = watch(state, { count: (v) => console.log('Watcher 1:', v) });
// Feature 2
const unwatch2 = watch(state, { count: (v) => console.log('Watcher 2:', v) });
// Feature 3
const stopEffect = effect(() => console.log('Effect:', state.count));
// Feature 4
const timer = setInterval(() => state.count++, 1000);
// Now cleanup manually - must remember all 4! 😰
unwatch1();
unwatch2();
stopEffect();
clearInterval(timer);At first glance, this seems straightforward. But there are hidden problems.
What's the Real Issue?
Create 4 features
↓
Get 4 cleanup functions
↓
Must remember to call ALL 4
↓
In the RIGHT order
↓
Forget one = memory leak 💥
Forget order = potential bugs 💥Problems: ❌ Must manually call each cleanup
❌ Easy to forget one
❌ Must remember correct order
❌ Tedious and error-prone
❌ Hard to maintain as features grow
The Solution with collector.cleanup()
With a collector, you add cleanups as you create features, then call cleanup() once:
const state = ReactiveUtils.state({ count: 0 });
const myCollector = collector();
// Feature 1
myCollector.add(watch(state, { count: (v) => console.log('Watcher 1:', v) }));
// Feature 2
myCollector.add(watch(state, { count: (v) => console.log('Watcher 2:', v) }));
// Feature 3
myCollector.add(effect(() => console.log('Effect:', state.count)));
// Feature 4
const timer = setInterval(() => state.count++, 1000);
myCollector.add(() => clearInterval(timer));
// One call to cleanup everything! 🎉
myCollector.cleanup();What just happened?
Create features
↓
Add each cleanup to collector
↓
Call cleanup() ONCE
↓
All cleanups run automatically
↓
In correct order
↓
Everything disposed! ✨Benefits: ✅ Single call cleans everything
✅ Can't forget individual cleanups
✅ Correct execution order guaranteed
✅ Clean, maintainable code
✅ Scales as features grow
Mental Model
Think of collector.cleanup() like end-of-day cleanup at a restaurant:
Without cleanup() (Manual Cleanup)
Restaurant Closing
├─ Turn off oven (remember!)
├─ Lock freezer (remember!)
├─ Clean grill (remember!)
├─ Turn off lights (remember!)
├─ Set alarm (remember!)
└─ Lock door (remember!)
Staff must remember all 6 tasks! 😰
Forget one = problem!With cleanup() (Checklist System)
Restaurant Closing
│
Press "Close Restaurant" button
↓
[Checklist Runs]
├─ ✓ Turn off oven
├─ ✓ Lock freezer
├─ ✓ Clean grill
├─ ✓ Turn off lights
├─ ✓ Set alarm
└─ ✓ Lock door
↓
All tasks done automatically! 🎉Key insight: Instead of remembering every cleanup task, you have a single "close restaurant" button (cleanup()) that runs through a checklist and does everything for you.
How Does It Work?
Under the Hood
When you call cleanup(), it iterates through all cleanup functions and executes them:
// Simplified implementation
function collector() {
const cleanups = [];
let isDisposed = false;
return {
add(cleanup) {
if (isDisposed) {
console.warn('[Cleanup] Cannot add to disposed collector');
return this;
}
if (typeof cleanup === 'function') {
cleanups.push(cleanup);
}
return this;
},
cleanup() {
// 1. Check if already disposed (prevent double cleanup)
if (isDisposed) return;
// 2. Mark as disposed
isDisposed = true;
// 3. Execute each cleanup function
cleanups.forEach(cleanup => {
try {
cleanup();
} catch (error) {
console.error('[Cleanup] Collector error:', error);
}
});
// 4. Clear the array
cleanups.length = 0;
},
get size() { return cleanups.length; },
get disposed() { return isDisposed; }
};
}What's happening:
1️⃣ Call cleanup()
↓
2️⃣ Check if already disposed
(if yes, return early)
↓
3️⃣ Set disposed flag to true
↓
4️⃣ Loop through cleanups array
↓
5️⃣ Execute each function (with error handling)
↓
6️⃣ Clear the array (set size to 0)
↓
7️⃣ Collector is now disposedExecution Order
Cleanups run in the order they were added:
myCollector.add(() => console.log('First'));
myCollector.add(() => console.log('Second'));
myCollector.add(() => console.log('Third'));
myCollector.cleanup();
// Output:
// First
// Second
// ThirdThis is usually what you want—resources are cleaned up in FIFO (first-in, first-out) order.
Basic Usage
Example 1: Simple Cleanup
const myCollector = collector();
// Add cleanups
myCollector.add(() => console.log('Step 1: Close connections'));
myCollector.add(() => console.log('Step 2: Save state'));
myCollector.add(() => console.log('Step 3: Clear cache'));
console.log('Before cleanup - size:', myCollector.size); // 3
console.log('Before cleanup - disposed:', myCollector.disposed); // false
// Run cleanup
myCollector.cleanup();
// Logs:
// Step 1: Close connections
// Step 2: Save state
// Step 3: Clear cache
console.log('After cleanup - size:', myCollector.size); // 0
console.log('After cleanup - disposed:', myCollector.disposed); // trueWhat's happening?
- Add three cleanup functions
- Check status before cleanup
- Call
cleanup()to run all functions - Check status after cleanup—size is 0, disposed is true
Example 2: Cleaning Up Watchers
const state = ReactiveUtils.state({ count: 0, name: 'Alice' });
const myCollector = collector();
// Add watchers
myCollector.add(
watch(state, {
count: (val) => console.log('Count changed to:', val)
})
);
myCollector.add(
watch(state, {
name: (val) => console.log('Name changed to:', val)
})
);
// Test watchers
state.count = 5; // Logs: Count changed to: 5
state.name = 'Bob'; // Logs: Name changed to: Bob
// Clean up
console.log('Cleaning up', myCollector.size, 'watchers...');
myCollector.cleanup();
// Watchers are now stopped
state.count = 10; // Nothing logged
state.name = 'Charlie'; // Nothing logged
console.log('Watchers stopped!');What's happening?
- Watchers work normally before cleanup
cleanup()stops all watchers- State changes after cleanup don't trigger watchers
Example 3: Component Lifecycle
class Dashboard {
constructor() {
this.collector = collector();
this.state = state({
data: [],
loading: false
});
// Add watcher
this.collector.add(
watch(this.state, {
loading: (val) => {
console.log('Loading:', val);
this.updateUI();
}
})
);
// Add effect
this.collector.add(
effect(() => {
console.log('Data items:', this.state.data.length);
})
);
// Add timer
const timer = setInterval(() => {
this.state.data.push({ id: Date.now() });
}, 2000);
this.collector.add(() => {
console.log('Stopping timer');
clearInterval(timer);
});
console.log('Dashboard initialized with', this.collector.size, 'cleanups');
}
updateUI() {
console.log('UI updated');
}
destroy() {
console.log('Destroying dashboard...');
this.collector.cleanup();
console.log('Dashboard destroyed!');
}
}
// Usage
const dashboard = new Dashboard();
// Dashboard initialized with 3 cleanups
// Data items: 0
setTimeout(() => {
dashboard.destroy();
// Destroying dashboard...
// Stopping timer
// Dashboard destroyed!
}, 5000);What's happening?
- Constructor creates features and adds cleanups
- Each feature works normally
destroy()callscleanup()to dispose everything- One call stops all watchers, effects, and timers
Deep Dive: Cleanup Execution
Synchronous Execution
All cleanups run synchronously in sequence:
const myCollector = collector();
myCollector.add(() => {
console.log('Cleanup 1 start');
// Some work...
console.log('Cleanup 1 end');
});
myCollector.add(() => {
console.log('Cleanup 2 start');
// Some work...
console.log('Cleanup 2 end');
});
console.log('Before cleanup');
myCollector.cleanup();
console.log('After cleanup');
// Output:
// Before cleanup
// Cleanup 1 start
// Cleanup 1 end
// Cleanup 2 start
// Cleanup 2 end
// After cleanupKey insight: cleanup() blocks until all functions finish. No async/await needed.
Async Cleanups (Not Recommended)
While cleanup functions can be async, cleanup() doesn't await them:
const myCollector = collector();
myCollector.add(async () => {
console.log('Async cleanup start');
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Async cleanup end (1 second later)');
});
myCollector.add(() => {
console.log('Sync cleanup');
});
console.log('Before cleanup');
myCollector.cleanup();
console.log('After cleanup (immediate)');
// Output:
// Before cleanup
// Async cleanup start
// Sync cleanup
// After cleanup (immediate)
// Async cleanup end (1 second later) ← Still runs!What's happening:
- Async cleanup starts but doesn't block
- Sync cleanup runs immediately
cleanup()returns before async finishes- Async cleanup completes in background
Best practice: Keep cleanup functions synchronous when possible.
Order-Dependent Cleanup
Sometimes cleanup order matters:
const myCollector = collector();
// Database connection
let dbConnection = null;
myCollector.add(() => {
console.log('Opening database...');
dbConnection = { connected: true };
});
// Database operations
myCollector.add(() => {
if (dbConnection && dbConnection.connected) {
console.log('Closing database...');
dbConnection.connected = false;
dbConnection = null;
}
});
// File operations (depend on database)
myCollector.add(() => {
console.log('Cleaning up files...');
// Files use database, so this should run first
});
// PROBLEM: Wrong order!
myCollector.cleanup();
// Cleaning up files... (tries to use database)
// Closing database... (database closed too late!)
// Opening database... (this shouldn't run!)Solution: Add in reverse order:
const myCollector = collector();
let dbConnection = null;
// Add in reverse order of what you want to happen
// 3. Last to cleanup (opened first)
myCollector.add(() => {
console.log('Opening database...');
dbConnection = { connected: true };
});
// 2. Second to cleanup
myCollector.add(() => {
if (dbConnection && dbConnection.connected) {
console.log('Closing database...');
dbConnection.connected = false;
dbConnection = null;
}
});
// 1. First to cleanup (opened last)
myCollector.add(() => {
console.log('Cleaning up files...');
});
myCollector.cleanup();
// Opening database...
// Closing database...
// Cleaning up files... ✓ Correct order!Error Handling
Errors Don't Stop Other Cleanups
If one cleanup throws an error, others still run:
const myCollector = collector();
myCollector.add(() => {
console.log('Cleanup 1: OK');
});
myCollector.add(() => {
console.log('Cleanup 2: ERROR!');
throw new Error('Something went wrong!');
});
myCollector.add(() => {
console.log('Cleanup 3: OK (still runs!)');
});
myCollector.cleanup();
// Output:
// Cleanup 1: OK
// Cleanup 2: ERROR!
// [Error logged to console]
// Cleanup 3: OK (still runs!)What's happening:
- Each cleanup is wrapped in a try-catch
- Errors are logged but don't stop execution
- All cleanups get a chance to run
Why this is good:
- One broken cleanup doesn't prevent others
- More resilient to errors
- Better for debugging (all cleanups attempt to run)
Handling Errors Gracefully
Best practice: Handle errors inside your cleanup functions:
const myCollector = collector();
myCollector.add(() => {
try {
console.log('Risky cleanup...');
// Something that might fail
if (Math.random() > 0.5) {
throw new Error('Failed!');
}
console.log('Success!');
} catch (error) {
console.log('Handled error:', error.message);
}
});
myCollector.cleanup();
// Either:
// Risky cleanup... Success!
// Or:
// Risky cleanup... Handled error: Failed!Common Patterns
Pattern 1: Cleanup on Unmount
function createComponent() {
const myCollector = collector();
const state = ReactiveUtils.state({ visible: true });
// Setup
myCollector.add(
watch(state, {
visible: (val) => console.log('Visibility:', val)
})
);
return {
state,
unmount() {
console.log('Component unmounting...');
myCollector.cleanup();
console.log('Component unmounted!');
}
};
}
const component = createComponent();
component.state.visible = false; // Logs: Visibility: false
component.unmount();
// Logs: Component unmounting... Component unmounted!Pattern 2: Auto-Cleanup After Timeout
function createTemporaryFeature(duration = 5000) {
const myCollector = collector();
console.log('Feature started');
// Add some features
myCollector.add(() => console.log('Cleanup A'));
myCollector.add(() => console.log('Cleanup B'));
// Auto-cleanup after duration
setTimeout(() => {
console.log('Auto-cleanup triggered');
myCollector.cleanup();
}, duration);
return {
manualCleanup: () => myCollector.cleanup()
};
}
const feature = createTemporaryFeature(3000);
// Feature started
// ... 3 seconds later ...
// Auto-cleanup triggered
// Cleanup A
// Cleanup BPattern 3: Conditional Cleanup
function createFeature(options) {
const myCollector = collector();
let cleanedUp = false;
myCollector.add(() => console.log('Cleanup 1'));
myCollector.add(() => console.log('Cleanup 2'));
return {
cleanup(force = false) {
if (cleanedUp) {
console.log('Already cleaned up');
return;
}
if (!force && !options.allowCleanup) {
console.log('Cleanup not allowed');
return;
}
console.log('Cleaning up...');
myCollector.cleanup();
cleanedUp = true;
}
};
}
const feature = createFeature({ allowCleanup: false });
feature.cleanup(); // Logs: Cleanup not allowed
feature.cleanup(true); // Logs: Cleaning up... Cleanup 1, Cleanup 2
feature.cleanup(); // Logs: Already cleaned upPattern 4: Cleanup with Logging
function createMonitoredFeature() {
const myCollector = collector();
const startTime = Date.now();
myCollector.add(() => {
console.log('Cleanup: Resource 1');
});
myCollector.add(() => {
console.log('Cleanup: Resource 2');
});
myCollector.add(() => {
console.log('Cleanup: Resource 3');
});
return {
cleanup() {
const elapsed = Date.now() - startTime;
console.log(`Starting cleanup after ${elapsed}ms`);
console.log(`Cleaning up ${myCollector.size} resources`);
myCollector.cleanup();
console.log('Cleanup complete');
}
};
}
const feature = createMonitoredFeature();
setTimeout(() => feature.cleanup(), 2000);
// Starting cleanup after 2003ms
// Cleaning up 3 resources
// Cleanup: Resource 1
// Cleanup: Resource 2
// Cleanup: Resource 3
// Cleanup completeEdge Cases and Gotchas
Gotcha 1: Multiple Calls (Safe)
const myCollector = collector();
myCollector.add(() => console.log('Cleanup'));
myCollector.cleanup(); // Runs cleanup
// Logs: Cleanup
myCollector.cleanup(); // Does nothing (already disposed)
myCollector.cleanup(); // Does nothing
myCollector.cleanup(); // Does nothing
console.log(myCollector.disposed); // trueWhat's happening:
- First
cleanup()runs cleanups and setsdisposedflag - Subsequent calls check
disposedand return early - Safe to call multiple times—no double cleanup
Gotcha 2: Empty Collector
const myCollector = collector();
// Nothing added
myCollector.cleanup(); // Runs fine, does nothing
console.log(myCollector.size); // 0
console.log(myCollector.disposed); // trueWhat's happening:
- Calling
cleanup()on empty collector is fine - Sets
disposedflag even with no cleanups - No errors or warnings
Gotcha 3: Adding After Cleanup
const myCollector = collector();
myCollector.add(() => console.log('Cleanup 1'));
myCollector.cleanup();
// Logs: Cleanup 1
// Try to add after cleanup
myCollector.add(() => console.log('Cleanup 2'));
// Warning: Cannot add to disposed collector
console.log(myCollector.size); // 0 (not added)What's happening:
- Once disposed, no more cleanups can be added
add()calls are ignored with a warning- Check
disposedbefore adding
Gotcha 4: Cleanup Within Cleanup
const myCollector = collector();
myCollector.add(() => {
console.log('Cleanup 1');
// Don't do this!
myCollector.cleanup();
});
myCollector.add(() => {
console.log('Cleanup 2 (never runs!)');
});
myCollector.cleanup();
// Logs: Cleanup 1
// Cleanup 2 never runs because cleanup() was called recursivelyWhat's happening:
- Calling
cleanup()inside a cleanup sets thedisposedflag - Remaining cleanups are skipped
- Never call
cleanup()from within a cleanup function
Summary
Key Takeaways
✅ cleanup() is an instance method that runs all cleanup functions
✅ Executes in order—cleanups run in the sequence they were added
✅ Marks collector as disposed—further adds are rejected
✅ Clears the cleanup array—size becomes 0 after cleanup
✅ Error-safe—one cleanup error doesn't prevent others from running
✅ Safe to call multiple times—subsequent calls do nothing
✅ Synchronous execution—blocks until all cleanups finish
Quick Reference
// Create and populate collector
const myCollector = collector();
myCollector.add(() => console.log('Cleanup 1'));
myCollector.add(() => console.log('Cleanup 2'));
myCollector.add(() => console.log('Cleanup 3'));
// Execute all cleanups
myCollector.cleanup();
// Logs: Cleanup 1, Cleanup 2, Cleanup 3
// Check status
console.log(myCollector.size); // 0 (cleared)
console.log(myCollector.disposed); // true
// Safe to call again
myCollector.cleanup(); // Does nothingOne-Line Rule
Call
cleanup()when you're done with a feature to run all cleanup functions, free resources, and mark the collector as disposed—one call handles everything.
Next Steps:
- Learn about
collector.add()to add cleanup functions - Learn about
collector.sizeto check cleanup count - Learn about
collector.disposedto check if disposed