Understanding builder.build() - A Beginner's Guide
Quick Start (30 seconds)
Need to finalize your reactive builder? Use builder.build():
// Create and configure a reactive builder
const builder = reactive({ count: 0, name: 'Counter' })
.computed({
doubled() {
return this.state.count * 2;
}
})
.action('increment', (state) => {
state.count++;
});
// Build the final reactive object
const counter = builder.build();
// Now use it as a normal reactive object
console.log(counter.count); // 0
console.log(counter.doubled); // 0
console.log(counter.name); // "Counter"
counter.increment();
console.log(counter.count); // 1
console.log(counter.doubled); // 2
// Clean up when done
counter.destroy();That's it! builder.build() finalizes the builder and returns the reactive state object with a destroy() method!
What is builder.build()?
builder.build() is a builder method that finalizes the building process and returns the final reactive state object. It's the last step in the builder pattern - you configure your state with chainable methods, then call .build() to get the finished product.
This method:
- Finalizes the building process
- Returns the reactive state object (not the builder)
- Adds a
destroy()method for cleanup - Should be called last in the chain
- Returns an object you can actually use
Think of it as finishing construction - after adding all your features with the builder, you call .build() to get the completed reactive object ready to use.
Syntax
// Finalize the builder
builder.build()
// Full example
const myObject = reactive({ count: 0 })
.computed({ doubled() { return this.state.count * 2; } })
.action('increment', (state) => state.count++)
.build() // ← Finalize and get the reactive objectParameters:
- None -
.build()takes no parameters
Returns:
- The reactive state object with all configured features
- Includes a
destroy()method for cleanup
Important:
- Should be the last method called in the chain
- Returns the state object (not the builder)
- The returned object has all state properties, computed properties, and actions
- The returned object has a
destroy()method for cleanup
Why Does This Exist?
The Problem with Just the Builder
Let's say you configure a reactive builder but forget to call .build():
// Configure a builder
const counter = reactive({ count: 0 })
.computed({
doubled() {
return this.state.count * 2;
}
})
.action('increment', (state) => {
state.count++;
});
// Oops! Forgot to call .build()
// Try to use it
console.log(counter.count); // undefined ❌
console.log(counter.doubled); // undefined ❌
counter.increment(); // undefined (not a function) ❌
// What happened?
console.log(counter); // It's the BUILDER, not the state!What's the Real Issue?
Without .build():
┌─────────────────────┐
│ Builder Object │
│ .state (internal) │ ← State is hidden
│ .computed() │ ← These are builder methods
│ .action() │ ← Not the final object
│ .build() │
└─────────────────────┘
│
▼
Can't use it directly!
Properties not accessible!
Actions not available!Problems: ❌ Builder is not the final object ❌ Can't access state properties directly ❌ Actions aren't methods yet ❌ Computed properties not accessible ❌ No destroy() method ❌ Confusing to work with
The Solution with builder.build()
When you call builder.build(), you get the final reactive object:
// Configure and build
const counter = reactive({ count: 0 })
.computed({
doubled() {
return this.state.count * 2;
}
})
.action('increment', (state) => {
state.count++;
})
.build(); // ← Get the final object
// Now use it normally
console.log(counter.count); // 0 ✅
console.log(counter.doubled); // 0 ✅
counter.increment(); // Works! ✅
console.log(counter.count); // 1 ✅What Just Happened?
With .build():
┌─────────────────────┐
│ Builder Object │
│ (configuration) │
└──────────┬──────────┘
│
▼ .build()
┌─────────────────────┐
│ Final State Object │
│ count: 0 │ ← Direct access
│ doubled (computed) │ ← Computed property
│ increment() │ ← Action method
│ destroy() │ ← Cleanup method
└─────────────────────┘
│
▼
Ready to use!
Everything accessible!
Clean API!With builder.build():
- Get the final reactive object
- Direct access to state properties
- Computed properties available
- Actions as methods
destroy()method for cleanup- Clean, usable API
Benefits: ✅ Finalizes the building process ✅ Returns the usable reactive object ✅ Adds destroy() for cleanup ✅ Clean separation: configure vs use ✅ Clear API boundary ✅ Proper resource management
Mental Model
Think of builder.build() like finishing a house and getting the keys:
Building Phase (Builder):
┌─────────────────────┐
│ Construction Site │
│ 🏗️ Under Construction
│ │
│ - Adding walls │
│ - Installing roof │
│ - Wiring electric │
│ - Plumbing │
└─────────────────────┘
Not ready to use!
Still building!
Calling .build() (Get Keys):
┌─────────────────────┐
│ Construction Site │
└──────────┬──────────┘
│ .build()
│ 🔨 Finalize
│ 🧹 Clean up
│ 🔑 Hand over keys
▼
┌─────────────────────┐
│ Finished House │
│ 🏠 Ready to Live In │
│ │
│ - Enter home │
│ - Use rooms │
│ - Turn on lights │
│ - Run water │
└─────────────────────┘
Ready to use!
Move in!Key Insight: Just like a house under construction isn't ready to live in until it's finished and you get the keys, a builder isn't ready to use until you call .build() to get the final reactive object!
How Does It Work?
The Magic: Finalizing and Returning State
When you call builder.build(), here's what happens behind the scenes:
// What you write:
const counter = reactive({ count: 0 })
.computed({ doubled() { return this.state.count * 2; } })
.action('increment', (state) => state.count++)
.build();
// What actually happens (simplified):
class Builder {
constructor(initialState) {
this.state = reactive(initialState);
this.cleanups = [];
}
computed(defs) {
// Add computed properties
addComputedToState(this.state, defs);
return this; // Return builder for chaining
}
action(name, fn) {
// Add action method
this.state[name] = (...args) => fn(this.state, ...args);
return this; // Return builder for chaining
}
build() {
// 1. Add destroy() method
this.state.destroy = () => {
// 2. Run all cleanup functions
this.cleanups.forEach(cleanup => cleanup());
// 3. Clear cleanups array
this.cleanups = [];
};
// 4. Return the state (not the builder!)
return this.state;
}
}In other words: builder.build():
- Adds a
destroy()method to the state destroy()runs all stored cleanup functions- Returns the reactive state object
- The builder's job is done
Under the Hood
.build()
│
▼
┌───────────────────────┐
│ Add destroy() │
│ to state object │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ destroy() contains │
│ all cleanup logic │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Return State Object │
│ (not builder!) │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Ready to Use! │
│ - State properties │
│ - Computed props │
│ - Actions │
│ - destroy() │
└───────────────────────┘What happens:
1️⃣ .build() is called 2️⃣ destroy() method is added to state 3️⃣ State object is returned (not builder) 4️⃣ You can use the returned object 5️⃣ Call destroy() when done to cleanup
Basic Usage
Simple Build
The simplest way to use builder.build():
// Create and build
const builder = reactive({ count: 0 });
const counter = builder.build();
// Use the built object
console.log(counter.count); // 0
counter.count = 5;
console.log(counter.count); // 5Build After Chaining
Most commonly, you build after adding features:
// Chain methods, then build
const counter = reactive({ count: 0 })
.computed({
doubled() {
return this.state.count * 2;
}
})
.action('increment', (state) => {
state.count++;
})
.action('decrement', (state) => {
state.count--;
})
.build(); // ← Build at the end
// Use all features
console.log(counter.count); // 0
console.log(counter.doubled); // 0
counter.increment();
console.log(counter.count); // 1
console.log(counter.doubled); // 2Build Without Extra Features
You can build immediately without adding anything:
// Just basic state
const app = reactive({ name: 'App', version: '1.0' })
.build();
console.log(app.name); // "App"
console.log(app.version); // "1.0"What build() Returns
The Returned Object Has:
- All state properties
const obj = reactive({ count: 0, name: 'Test' })
.build();
console.log(obj.count); // 0
console.log(obj.name); // "Test"- All computed properties
const obj = reactive({ count: 0 })
.computed({
doubled() {
return this.state.count * 2;
}
})
.build();
console.log(obj.doubled); // 0 (computed property)- All actions as methods
const obj = reactive({ count: 0 })
.action('increment', (state) => state.count++)
.build();
obj.increment(); // Method available- A destroy() method
const obj = reactive({ count: 0 })
.build();
obj.destroy(); // Cleanup method availableComplete Example
const app = reactive({ count: 0, name: 'Counter' })
.computed({
doubled() {
return this.state.count * 2;
}
})
.watch({
count(newVal) {
console.log('Count:', newVal);
}
})
.action('increment', (state) => {
state.count++;
})
.build();
// The returned object has:
console.log(app.count); // State property
console.log(app.name); // State property
console.log(app.doubled); // Computed property
app.increment(); // Action method
app.destroy(); // Cleanup methodThe destroy() Method
What is destroy()?
destroy() is a method added to the built object that cleans up all effects, watchers, and bindings.
const app = reactive({ count: 0 })
.effect(() => {
console.log('Count:', app.state.count);
})
.build();
// Logs: "Count: 0"
app.count = 5;
// Logs: "Count: 5"
// Clean up all effects
app.destroy();
app.count = 10;
// Nothing logged (effects destroyed)When to Call destroy()
Call destroy() when:
- Component unmounts (in frameworks)
- User navigates away
- Cleaning up temporary objects
- Preventing memory leaks
- Stopping all reactive updates
// Example: Component lifecycle
class MyComponent {
constructor() {
this.state = reactive({ count: 0 })
.effect(() => {
this.render();
})
.build();
}
render() {
console.log('Rendering...', this.state.count);
}
destroy() {
// Clean up when component is removed
this.state.destroy();
}
}
const component = new MyComponent();
component.state.count = 5; // Renders
component.destroy(); // Clean up
component.state.count = 10; // Doesn't renderWhat destroy() Cleans Up
const app = reactive({ count: 0 })
// This effect will be cleaned up
.effect(() => {
console.log('Effect:', app.state.count);
})
// This watcher will be cleaned up
.watch({
count(newVal) {
console.log('Watch:', newVal);
}
})
// This binding will be cleaned up
.bind({
'#counter': 'count'
})
.build();
// All effects, watchers, and bindings are active
app.count = 5;
// Clean up everything
app.destroy();
// No effects, watchers, or bindings run anymore
app.count = 10;Common Patterns
Pattern: One-Line Build
// Configure and build in one statement
const counter = reactive({ count: 0 })
.action('increment', (state) => state.count++)
.build();
counter.increment();Pattern: Separate Configure and Build
// Configure separately
let builder = reactive({ count: 0 });
builder = builder.computed({
doubled() {
return this.state.count * 2;
}
});
builder = builder.action('increment', (state) => {
state.count++;
});
// Build later
const counter = builder.build();Pattern: Conditional Building
const config = { enableDebug: true };
let builder = reactive({ count: 0 })
.action('increment', (state) => state.count++);
if (config.enableDebug) {
builder = builder.effect(() => {
console.log('Debug:', builder.state.count);
});
}
const counter = builder.build();Pattern: Factory Function
function createCounter(initialCount = 0) {
return reactive({ count: initialCount })
.computed({
doubled() {
return this.state.count * 2;
}
})
.actions({
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
},
reset(state) {
state.count = initialCount;
}
})
.build();
}
const counter1 = createCounter(0);
const counter2 = createCounter(10);
const counter3 = createCounter(100);Pattern: Cleanup on Unmount
// React-style component
function useCounter() {
const [counter] = React.useState(() =>
reactive({ count: 0 })
.action('increment', (state) => state.count++)
.build()
);
// Cleanup when component unmounts
React.useEffect(() => {
return () => counter.destroy();
}, []);
return counter;
}Pattern: Temporary Reactive Objects
function processData(data) {
// Create temporary reactive object
const processor = reactive({ progress: 0, result: null })
.effect(() => {
document.getElementById('progress').textContent = processor.state.progress;
})
.build();
// Process data
for (let i = 0; i < data.length; i++) {
processor.progress = (i / data.length) * 100;
// ... process data[i] ...
}
processor.progress = 100;
// Clean up when done
processor.destroy();
return processor.result;
}Common Pitfalls
Pitfall #1: Forgetting to Call .build()
❌ Wrong:
// Forgot to call .build()
const counter = reactive({ count: 0 })
.action('increment', (state) => state.count++);
// This is the BUILDER, not the final object
console.log(counter.count); // undefined
counter.increment(); // undefined (not a function)✅ Correct:
// Call .build() to get the final object
const counter = reactive({ count: 0 })
.action('increment', (state) => state.count++)
.build(); // ← Don't forget this!
console.log(counter.count); // 0 ✅
counter.increment(); // Works! ✅Pitfall #2: Trying to Chain After .build()
❌ Wrong:
const counter = reactive({ count: 0 })
.action('increment', (state) => state.count++)
.build()
.action('decrement', (state) => state.count--); // Error!.build() returns the state object, not the builder, so you can't chain builder methods after it.
✅ Correct:
const counter = reactive({ count: 0 })
.action('increment', (state) => state.count++)
.action('decrement', (state) => state.count--) // Before .build()
.build(); // ← .build() is lastPitfall #3: Using builder.state After .build()
❌ Wrong:
const counter = reactive({ count: 0 })
.build();
// Trying to access via .state
console.log(counter.state.count); // undefinedAfter building, access properties directly on the returned object.
✅ Correct:
const counter = reactive({ count: 0 })
.build();
// Access properties directly
console.log(counter.count); // 0 ✅Pitfall #4: Not Calling destroy() When Done
⚠️ Memory Leak:
function createAndUseCounter() {
const counter = reactive({ count: 0 })
.effect(() => {
console.log('Count:', counter.state.count);
})
.build();
counter.count = 5;
// Oops! Forgot to call destroy()
// Effect keeps running forever!
}
createAndUseCounter(); // Creates effect that never gets cleaned up✅ Correct:
function createAndUseCounter() {
const counter = reactive({ count: 0 })
.effect(() => {
console.log('Count:', counter.state.count);
})
.build();
counter.count = 5;
// Clean up when done
counter.destroy(); // ✅
}
createAndUseCounter(); // Effect is properly cleaned upPitfall #5: Calling .build() Multiple Times
❌ Unnecessary:
const builder = reactive({ count: 0 })
.action('increment', (state) => state.count++);
const counter1 = builder.build();
const counter2 = builder.build(); // Same as counter1
// Both reference the same state
counter1.count = 5;
console.log(counter2.count); // 5 (same object)Calling .build() multiple times on the same builder returns the same state object.
✅ Better:
// Build once
const builder = reactive({ count: 0 })
.action('increment', (state) => state.count++);
const counter = builder.build();
// If you need multiple instances, create multiple builders
const counter1 = reactive({ count: 0 })
.action('increment', (state) => state.count++)
.build();
const counter2 = reactive({ count: 0 })
.action('increment', (state) => state.count++)
.build();
// Now they're independent
counter1.count = 5;
console.log(counter2.count); // 0 (different objects)Summary
What is builder.build()?
builder.build() is a builder method that finalizes the building process and returns the final reactive state object with all configured features and a destroy() method.
Why use builder.build()?
- Finalizes the builder
- Returns the usable reactive object
- Adds
destroy()for cleanup - Marks the end of configuration
- Necessary to actually use the object
Key Points to Remember:
1️⃣ Call last - .build() should be the last method in the chain 2️⃣ Returns state - Returns the state object, not the builder 3️⃣ Adds destroy() - The returned object has a destroy() method 4️⃣ Can't chain after - Can't call builder methods after .build() 5️⃣ Must be called - Builder is not usable until you call .build()
Mental Model: Think of builder.build() as finishing construction and getting the keys - you can't live in the house until construction is complete and you receive the keys!
Quick Reference:
// BASIC BUILD
const obj = reactive({ count: 0 })
.build();
// BUILD AFTER CONFIGURATION
const obj = reactive({ count: 0 })
.computed({ doubled() { return this.state.count * 2; } })
.watch({ count(n) { console.log(n); } })
.action('increment', (state) => state.count++)
.build(); // ← Must be last
// USE THE BUILT OBJECT
console.log(obj.count); // State property
console.log(obj.doubled); // Computed property
obj.increment(); // Action method
// CLEANUP WHEN DONE
obj.destroy(); // Clean up all effects/watchers/bindings
// ❌ DON'T DO THIS
const wrong = reactive({ count: 0 })
.build()
.action('inc', (state) => state.count++); // Error!
// ❌ DON'T FORGET THIS
const builder = reactive({ count: 0 })
.action('inc', (state) => state.count++);
// Forgot .build()!
builder.increment(); // Won't work!
// ✅ DO THIS
const correct = reactive({ count: 0 })
.action('inc', (state) => state.count++)
.build(); // Don't forget!
correct.increment(); // Works!Remember: builder.build() is the final step that transforms your configured builder into a usable reactive object. Always call it last, and always call destroy() when you're done to clean up!