Understanding builder.destroy() - A Beginner's Guide
Quick Start (30 seconds)
Need to clean up a reactive builder or built object? Use destroy():
// Create a reactive builder with effects
const builder = reactive({ count: 0 })
.effect(() => {
console.log('Count is:', builder.state.count);
})
.watch({
count(newVal) {
console.log('Count changed to:', newVal);
}
});
// Logs immediately: "Count is: 0"
// Update state - effects and watchers run
builder.state.count = 5;
// Logs: "Count changed to: 5"
// Logs: "Count is: 5"
// Clean up all effects and watchers
builder.destroy();
// Now effects and watchers won't run
builder.state.count = 10;
// Nothing logged (destroyed)
// Or with a built object:
const counter = reactive({ count: 0 })
.effect(() => console.log('Count:', counter.state.count))
.build();
counter.count = 5; // Effect runs
counter.destroy(); // Clean up
counter.count = 10; // Effect doesn't runThat's it! builder.destroy() stops all effects, watchers, and bindings!
What is builder.destroy()?
builder.destroy() is a method that stops all reactive effects, watchers, and bindings created during the building process. It's available both on the builder (before calling .build()) and on the built object (after calling .build()).
This method:
- Stops all effects from running
- Removes all watchers
- Cleans up all DOM bindings
- Prevents memory leaks
- Should be called when you're done with the object
- Can be called on builder or built object
Think of it as turning off the power - all the reactive connections stop working, effects stop running, and everything is cleaned up.
Syntax
// On a builder (before .build())
builder.destroy()
// On a built object (after .build())
builtObject.destroy()
// Full example
const builder = reactive({ count: 0 })
.effect(() => console.log(builder.state.count));
builder.destroy(); // Clean up the builder
// Or after building:
const counter = reactive({ count: 0 })
.effect(() => console.log(counter.state.count))
.build();
counter.destroy(); // Clean up the built objectParameters:
- None -
destroy()takes no parameters
Returns:
- Nothing (
undefined)
Important:
- Stops all effects, watchers, and bindings
- Can be called on builder or built object
- Safe to call multiple times
- Irreversible - effects won't restart
Why Does This Exist?
The Problem with Endless Effects
Let's say you create reactive effects but never clean them up:
function createCounter() {
const counter = reactive({ count: 0 })
.effect(() => {
console.log('Count:', counter.state.count);
// This effect keeps running FOREVER!
})
.build();
counter.count = 5;
return counter;
}
// Create multiple counters
const counter1 = createCounter(); // Creates effect
const counter2 = createCounter(); // Creates another effect
const counter3 = createCounter(); // Creates another effect
// All effects are still running, consuming memory!
// This causes a memory leak!What's the Real Issue?
Without destroy():
┌─────────────────────┐
│ Create Object 1 │
│ + Effect 1 ────────┼──→ Running forever
└─────────────────────┘
┌─────────────────────┐
│ Create Object 2 │
│ + Effect 2 ────────┼──→ Running forever
└─────────────────────┘
┌─────────────────────┐
│ Create Object 3 │
│ + Effect 3 ────────┼──→ Running forever
└─────────────────────┘
All effects keep running!
Memory leak!
Performance degrades!Problems: ❌ Effects run forever ❌ Memory leaks ❌ Wasted CPU cycles ❌ Performance degradation ❌ Can't stop unwanted updates ❌ No way to clean up
The Solution with destroy()
When you call destroy(), all effects stop:
function createCounter() {
const counter = reactive({ count: 0 })
.effect(() => {
console.log('Count:', counter.state.count);
})
.build();
counter.count = 5;
// Clean up when done
counter.destroy(); // ← Stop the effect
return counter;
}
// Create multiple counters
const counter1 = createCounter(); // Effect runs, then cleaned up
const counter2 = createCounter(); // Effect runs, then cleaned up
const counter3 = createCounter(); // Effect runs, then cleaned up
// No memory leaks!
// All effects properly cleaned up!What Just Happened?
With destroy():
┌─────────────────────┐
│ Create Object 1 │
│ + Effect 1 │──→ Running
└──────────┬──────────┘
│ .destroy()
▼
Stopped ✓
┌─────────────────────┐
│ Create Object 2 │
│ + Effect 2 │──→ Running
└──────────┬──────────┘
│ .destroy()
▼
Stopped ✓
Clean memory!
No leaks!
Good performance!With destroy():
- Effects stop running
- Memory is freed
- No performance degradation
- Clean, proper resource management
- No memory leaks
- Full control over lifecycle
Benefits: ✅ Stops all effects and watchers ✅ Prevents memory leaks ✅ Frees up resources ✅ Better performance ✅ Proper lifecycle management ✅ Clean shutdown
Mental Model
Think of destroy() like turning off the power in a building:
Without destroy() (Power On Forever):
┌─────────────────────┐
│ Building │
│ ⚡ Power ON │
│ │
│ 💡 Lights on │
│ 🌡️ AC running │
│ 🔊 Systems active │
│ 💻 Computers on │
└─────────────────────┘
Running forever!
Wasting energy!
Costs money!
With destroy() (Turn Off Power):
┌─────────────────────┐
│ Building │
│ 🔌 Power OFF │
│ │
│ ⚫ Lights off │
│ ❄️ AC stopped │
│ 🔇 Systems off │
│ ⏻ Computers off │
└─────────────────────┘
Everything stopped!
Energy saved!
Clean shutdown!Key Insight: Just like turning off the power in a building stops all electrical systems from running, destroy() stops all reactive effects, watchers, and bindings from running!
How Does It Work?
The Magic: Cleanup Functions
When you call destroy(), here's what happens behind the scenes:
// What you write:
const counter = reactive({ count: 0 })
.effect(() => {
console.log('Count:', counter.state.count);
})
.build();
counter.destroy();
// What actually happens (simplified):
class Builder {
constructor(initialState) {
this.state = reactive(initialState);
this.cleanups = []; // Array to store cleanup functions
}
effect(fn) {
// Create effect and get cleanup function
const cleanup = createEffect(() => {
fn();
});
// Store cleanup for later
this.cleanups.push(cleanup);
return this; // Return builder for chaining
}
destroy() {
// Run all cleanup functions
this.cleanups.forEach(cleanup => {
cleanup(); // Stops the effect
});
// Clear the cleanups array
this.cleanups = [];
}
build() {
// Add destroy() to the built object
this.state.destroy = () => this.destroy();
return this.state;
}
}In other words: destroy():
- Loops through all stored cleanup functions
- Calls each cleanup function
- Each cleanup stops its effect/watcher/binding
- Clears the cleanups array
- Everything stops running
Under the Hood
.destroy()
│
▼
┌───────────────────────┐
│ Loop Through │
│ Cleanup Functions │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Call Cleanup #1 │
│ (Stop Effect #1) │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Call Cleanup #2 │
│ (Stop Watcher #1) │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Call Cleanup #3 │
│ (Stop Binding #1) │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Clear Cleanups Array │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ All Stopped! │
│ ✓ No effects │
│ ✓ No watchers │
│ ✓ No bindings │
└───────────────────────┘What happens:
1️⃣ destroy() is called 2️⃣ All cleanup functions are executed 3️⃣ Each cleanup stops its effect/watcher/binding 4️⃣ Cleanups array is cleared 5️⃣ Everything stops running
Basic Usage
Destroy a Builder
Call destroy() on a builder before building:
// Create builder with effect
const builder = reactive({ count: 0 })
.effect(() => {
console.log('Count:', builder.state.count);
});
// Logs: "Count: 0"
builder.state.count = 5;
// Logs: "Count: 5"
// Destroy before building
builder.destroy();
builder.state.count = 10;
// Nothing logged (destroyed)Destroy a Built Object
Call destroy() on a built object:
// Create and build
const counter = reactive({ count: 0 })
.effect(() => {
console.log('Count:', counter.state.count);
})
.build();
// Logs: "Count: 0"
counter.count = 5;
// Logs: "Count: 5"
// Destroy the built object
counter.destroy();
counter.count = 10;
// Nothing logged (destroyed)Multiple Effects
destroy() stops all effects:
const app = reactive({ count: 0 })
.effect(() => {
console.log('Effect 1:', app.state.count);
})
.effect(() => {
console.log('Effect 2:', app.state.count * 2);
})
.effect(() => {
console.log('Effect 3:', app.state.count + 10);
})
.build();
// All 3 effects log
app.count = 5;
// All 3 effects log again
app.destroy();
app.count = 10;
// No effects log (all destroyed)What Gets Destroyed
Effects
All effects created with .effect() are stopped:
const app = reactive({ count: 0 })
.effect(() => {
console.log('Effect:', app.state.count);
})
.build();
app.count = 5; // Effect runs
app.destroy();
app.count = 10; // Effect doesn't runWatchers
All watchers created with .watch() are removed:
const app = reactive({ count: 0 })
.watch({
count(newVal, oldVal) {
console.log(`Watch: ${oldVal} → ${newVal}`);
}
})
.build();
app.count = 5; // Watcher runs
app.destroy();
app.count = 10; // Watcher doesn't runDOM Bindings
All DOM bindings created with .bind() are removed:
// HTML: <div id="counter"></div>
const app = reactive({ count: 0 })
.bind({
'#counter': 'count'
})
.build();
app.count = 5; // DOM updates
app.destroy();
app.count = 10; // DOM doesn't updateComputed Properties (Special Case)
Computed properties themselves aren't "destroyed", but their internal effects are cleaned up:
const app = reactive({ count: 0 })
.computed({
doubled() {
return this.state.count * 2;
}
})
.build();
console.log(app.doubled); // Works before destroy
app.destroy();
// Computed property still exists, but its internal tracking is cleaned up
console.log(app.doubled); // Still accessible, but no longer reactiveWhen to Call destroy()
Component Unmounting
// React example
function Counter() {
const [counter] = React.useState(() =>
reactive({ count: 0 })
.effect(() => {
document.title = `Count: ${counter.state.count}`;
})
.build()
);
// Clean up when component unmounts
React.useEffect(() => {
return () => {
counter.destroy(); // ← Important!
};
}, []);
return <div>{counter.count}</div>;
}Temporary Objects
function processData(items) {
// Create temporary reactive object
const processor = reactive({ progress: 0 })
.effect(() => {
console.log('Progress:', processor.state.progress + '%');
})
.build();
// Process items
items.forEach((item, index) => {
// Process item...
processor.progress = ((index + 1) / items.length) * 100;
});
// Clean up when done
processor.destroy(); // ← Important!
}Navigation
// Single Page App routing
const routes = {
'/home': () => {
const page = reactive({ data: null })
.effect(() => {
renderHomePage(page.state.data);
})
.build();
// Return cleanup function
return () => page.destroy();
},
'/about': () => {
const page = reactive({ data: null })
.effect(() => {
renderAboutPage(page.state.data);
})
.build();
return () => page.destroy();
}
};
let currentCleanup = null;
function navigate(route) {
// Clean up previous page
if (currentCleanup) {
currentCleanup(); // Destroys old page
}
// Create new page
currentCleanup = routes[route]();
}Testing
describe('Counter', () => {
let counter;
beforeEach(() => {
counter = reactive({ count: 0 })
.effect(() => {
console.log('Count:', counter.state.count);
})
.build();
});
afterEach(() => {
// Clean up after each test
counter.destroy(); // ← Important!
});
it('increments', () => {
counter.count = 5;
expect(counter.count).toBe(5);
});
});destroy() on Built Objects
Available on Built Objects
When you call .build(), a destroy() method is automatically added:
const counter = reactive({ count: 0 })
.effect(() => {
console.log('Count:', counter.state.count);
})
.build();
// The built object has destroy()
counter.destroy(); // ✅ AvailableSame as Builder destroy()
The destroy() on the built object does the same thing as the builder's destroy():
// These are equivalent:
// Option 1: Destroy builder
const builder = reactive({ count: 0 })
.effect(() => console.log('Count:', builder.state.count));
builder.destroy();
// Option 2: Destroy built object
const counter = reactive({ count: 0 })
.effect(() => console.log('Count:', counter.state.count))
.build();
counter.destroy();
// Both stop all effectsCommon Patterns
Pattern: Component Lifecycle
class Component {
constructor() {
this.state = reactive({ count: 0 })
.effect(() => this.render())
.build();
}
render() {
console.log('Rendering...', this.state.count);
}
unmount() {
this.state.destroy(); // Clean up
}
}
const component = new Component();
component.state.count = 5;
component.unmount(); // Clean up when donePattern: Scoped Reactive Objects
function withScope(fn) {
const scope = reactive({ data: null })
.effect(() => {
console.log('Data:', scope.state.data);
})
.build();
fn(scope);
// Auto-cleanup when scope ends
scope.destroy();
}
withScope((scope) => {
scope.data = 'Hello';
scope.data = 'World';
});
// Automatically cleaned up after functionPattern: Subscription Management
class EventBus {
constructor() {
this.subscriptions = new Map();
}
subscribe(event, handler) {
const subscription = reactive({ active: true })
.effect(() => {
if (subscription.state.active) {
handler();
}
})
.build();
this.subscriptions.set(event, subscription);
return () => {
subscription.destroy();
this.subscriptions.delete(event);
};
}
}
const bus = new EventBus();
const unsubscribe = bus.subscribe('update', () => {
console.log('Update event');
});
// Later...
unsubscribe(); // Destroys subscriptionPattern: Temporary State
async function fetchUserData(userId) {
// Create temporary reactive state
const fetcher = reactive({ loading: true, data: null, error: null })
.effect(() => {
if (fetcher.state.loading) {
showSpinner();
} else {
hideSpinner();
}
})
.build();
try {
const data = await fetch(`/api/users/${userId}`);
fetcher.data = await data.json();
} catch (error) {
fetcher.error = error.message;
} finally {
fetcher.loading = false;
}
const result = { data: fetcher.data, error: fetcher.error };
// Clean up temporary state
fetcher.destroy();
return result;
}Pattern: Resource Pool
class ResourcePool {
constructor() {
this.resources = [];
}
create() {
const resource = reactive({ inUse: false, data: null })
.effect(() => {
console.log('Resource:', resource.state.data);
})
.build();
this.resources.push(resource);
return resource;
}
destroyAll() {
this.resources.forEach(resource => {
resource.destroy();
});
this.resources = [];
}
}
const pool = new ResourcePool();
const r1 = pool.create();
const r2 = pool.create();
// Clean up all resources
pool.destroyAll();Common Pitfalls
Pitfall #1: Forgetting to Call destroy()
❌ Memory Leak:
function createManyCounters() {
for (let i = 0; i < 1000; i++) {
const counter = reactive({ count: 0 })
.effect(() => {
console.log('Counter', i, ':', counter.state.count);
})
.build();
// Oops! Never called destroy()
// All 1000 effects keep running forever!
}
}
createManyCounters(); // Memory leak!✅ Correct:
function createManyCounters() {
const counters = [];
for (let i = 0; i < 1000; i++) {
const counter = reactive({ count: 0 })
.effect(() => {
console.log('Counter', i, ':', counter.state.count);
})
.build();
counters.push(counter);
}
// Clean up all counters when done
counters.forEach(counter => counter.destroy());
}Pitfall #2: Using Object After destroy()
⚠️ Unexpected Behavior:
const counter = reactive({ count: 0 })
.effect(() => {
console.log('Count:', counter.state.count);
})
.build();
counter.destroy();
// Object still exists, but effects don't run
counter.count = 5;
// Nothing logged (effects destroyed)
console.log(counter.count); // 5 (state still works)The object still exists after destroy(), but effects/watchers/bindings don't run.
✅ Better Practice:
const counter = reactive({ count: 0 })
.effect(() => {
console.log('Count:', counter.state.count);
})
.build();
counter.destroy();
// Don't use the object after destroying
// counter = null; // Mark as destroyed if neededPitfall #3: Calling destroy() Multiple Times
✅ Safe (No Error):
const counter = reactive({ count: 0 })
.effect(() => {
console.log('Count:', counter.state.count);
})
.build();
counter.destroy();
counter.destroy(); // Safe - no error
counter.destroy(); // Safe - does nothingCalling destroy() multiple times is safe but unnecessary.
Pitfall #4: Not Destroying in Frameworks
❌ React Memory Leak:
function Counter() {
const counter = reactive({ count: 0 })
.effect(() => {
document.title = `Count: ${counter.state.count}`;
})
.build();
// Oops! No cleanup when component unmounts
return <div>{counter.count}</div>;
}✅ Correct:
function Counter() {
const [counter] = React.useState(() =>
reactive({ count: 0 })
.effect(() => {
document.title = `Count: ${counter.state.count}`;
})
.build()
);
// Clean up on unmount
React.useEffect(() => {
return () => counter.destroy();
}, []);
return <div>{counter.count}</div>;
}Pitfall #5: Destroying Too Early
❌ Wrong:
function process() {
const app = reactive({ progress: 0 })
.effect(() => {
console.log('Progress:', app.state.progress);
})
.build();
app.destroy(); // Too early!
// Effect won't run
app.progress = 50; // Nothing logged
app.progress = 100; // Nothing logged
}✅ Correct:
function process() {
const app = reactive({ progress: 0 })
.effect(() => {
console.log('Progress:', app.state.progress);
})
.build();
app.progress = 50; // Effect runs
app.progress = 100; // Effect runs
app.destroy(); // Destroy when done
}Summary
What is builder.destroy()?
builder.destroy() is a method that stops all reactive effects, watchers, and bindings to prevent memory leaks and free up resources.
Why use destroy()?
- Stop all effects and watchers
- Prevent memory leaks
- Free up resources
- Proper lifecycle management
- Clean shutdown
Key Points to Remember:
1️⃣ Stops everything - All effects, watchers, and bindings stop 2️⃣ Prevents leaks - Essential for avoiding memory leaks 3️⃣ Call when done - Call when you're finished with the object 4️⃣ Safe to call multiple times - No error if called repeatedly 5️⃣ Available on both - Works on builder and built object
Mental Model: Think of destroy() as turning off the power - all the reactive systems stop running, effects stop executing, and everything is cleanly shut down!
Quick Reference:
// DESTROY BUILDER
const builder = reactive({ count: 0 })
.effect(() => console.log('Count:', builder.state.count));
builder.destroy(); // Stop all effects
// DESTROY BUILT OBJECT
const counter = reactive({ count: 0 })
.effect(() => console.log('Count:', counter.state.count))
.build();
counter.destroy(); // Stop all effects
// COMPONENT CLEANUP
React.useEffect(() => {
return () => counter.destroy(); // Clean up on unmount
}, []);
// TEMPORARY OBJECT
function process() {
const temp = reactive({ data: null })
.effect(() => console.log(temp.state.data))
.build();
// ... use temp ...
temp.destroy(); // Clean up when done
}
// WHAT GETS DESTROYED
// ✓ All effects
// ✓ All watchers
// ✓ All bindings
// ✓ Internal cleanup functions
// AFTER DESTROY
counter.count = 5; // State still works
console.log(counter.count); // 5
// But effects/watchers/bindings don't runRemember: Always call destroy() when you're done with a reactive object to prevent memory leaks and ensure proper resource cleanup. It's especially important in component frameworks, temporary objects, and long-running applications!