Understanding builder.actions(defs) - A Beginner's Guide
Quick Start (30 seconds)
Need to add multiple methods at once? Use builder.actions():
// Create a reactive builder and add multiple actions
const counter = reactive({ count: 0, step: 1 })
.actions({
increment(state) {
state.count += state.step;
},
decrement(state) {
state.count -= state.step;
},
reset(state) {
state.count = 0;
},
add(state, amount) {
state.count += amount;
}
})
.build();
// Call actions to modify state
counter.increment();
console.log(counter.count); // 1
counter.add(5);
console.log(counter.count); // 6
counter.reset();
console.log(counter.count); // 0That's it! builder.actions() adds multiple named methods to your reactive object in one call and returns the builder for chaining!
What is builder.actions()?
builder.actions() is a builder method that adds multiple named methods (actions) to your reactive state at once. It's a convenient way to define several actions together using an object.
This method:
- Accepts an object of action definitions
- Each key becomes a method name
- Each value is the action function
- All actions receive state as first parameter
- Can accept additional parameters
- Can return values
Think of it as batch-adding behavior to your reactive object - you define all your actions in one place, and they all become methods on your state.
Syntax
// Add multiple actions to a builder
builder.actions(definitions)
// Full example
reactive({ count: 0 })
.actions({
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
}
})
.build()Parameters:
definitions- An object where:- Keys are action names (become method names)
- Values are functions that receive:
state- The reactive state (first parameter)- Additional parameters as needed
Returns:
- The builder (for method chaining)
Important:
- All action names must be valid JavaScript identifiers
- First parameter is always
statefor each action - Can use arrow functions or regular functions
- The builder is returned, so you can chain more methods
- Actions become methods on the built object
Why Does This Exist?
The Problem with Individual Action Calls
Let's say you want to add several actions using builder.action():
// Adding actions one by one
const counter = reactive({ count: 0 })
.action('increment', (state) => {
state.count++;
})
.action('decrement', (state) => {
state.count--;
})
.action('reset', (state) => {
state.count = 0;
})
.action('add', (state, amount) => {
state.count += amount;
})
.action('subtract', (state, amount) => {
state.count -= amount;
})
.build();This works, but can be verbose for many actions:
What's the Real Issue?
Multiple .action() Calls:
┌─────────────────────┐
│ .action('inc', fn) │ ← Repetitive
│ .action('dec', fn) │ ← Repetitive
│ .action('reset', fn)│ ← Repetitive
│ .action('add', fn) │ ← Repetitive
│ .action('sub', fn) │ ← Repetitive
└─────────────────────┘
Lots of .action()!
Verbose!
Scattered!Problems: ❌ Repetitive .action() calls ❌ Verbose for many actions ❌ Actions scattered across multiple lines ❌ Hard to see all actions at a glance ❌ More typing required
The Solution with builder.actions()
When you use builder.actions(), you define all actions in one object:
// Adding actions all at once
const counter = reactive({ count: 0 })
.actions({
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
},
reset(state) {
state.count = 0;
},
add(state, amount) {
state.count += amount;
},
subtract(state, amount) {
state.count -= amount;
}
})
.build();What Just Happened?
Single .actions() Call:
┌─────────────────────┐
│ .actions({ │
│ inc(state) {...}, │ ← All together
│ dec(state) {...}, │ ← Clear grouping
│ reset(state){...},│ ← Easy to see
│ add(state, n){...}│ ← Less repetition
│ }) │
└─────────────────────┘
One call!
Concise!
Grouped!With builder.actions():
- All actions in one place
- Less repetitive syntax
- Clear grouping of related actions
- Easy to see all available actions
- More concise code
- Same functionality as multiple
.action()calls
Benefits: ✅ Add multiple actions at once ✅ Less repetitive syntax ✅ Clear grouping in one object ✅ Easy to see all actions ✅ Chainable with other builder methods ✅ More concise for many actions
Mental Model
Think of builder.actions() like a control panel with all buttons at once:
Individual Buttons (One at a time):
┌─────────────────┐
│ Add Button 1 │ ← Install
└─────────────────┘
│
▼
┌─────────────────┐
│ Add Button 2 │ ← Install
└─────────────────┘
│
▼
┌─────────────────┐
│ Add Button 3 │ ← Install
└─────────────────┘
│
▼
┌─────────────────┐
│ Add Button 4 │ ← Install
└─────────────────┘
One at a time!
Repetitive!
Complete Control Panel (All at once):
┌─────────────────────┐
│ Control Panel │
│ ┌──────────────┐ │
│ │ [Button 1] │ │
│ │ [Button 2] │ │ ← Install all
│ │ [Button 3] │ │ at once!
│ │ [Button 4] │ │
│ └──────────────┘ │
└─────────────────────┘
All together!
Organized!Key Insight: Just like installing a complete control panel with all buttons at once instead of adding buttons one by one, builder.actions() lets you add all your actions together in one organized object!
How Does It Work?
The Magic: Batch Method Addition
When you call builder.actions(), here's what happens behind the scenes:
// What you write:
const counter = reactive({ count: 0 })
.actions({
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
}
})
.build();
// What actually happens (simplified):
// 1. Builder receives actions object
builder.actions({
increment(state) { state.count++; },
decrement(state) { state.count--; }
});
// 2. For each key-value pair:
Object.entries(actionsObject).forEach(([name, fn]) => {
// 3. Add method to state (same as builder.action())
state[name] = function(...args) {
return fn(state, ...args);
};
});
// 4. All methods now available on state
// 5. Return builder for chaining
return builder;In other words: builder.actions():
- Takes an object of action definitions
- Loops through each key-value pair
- For each pair, adds a method to state (like
builder.action()) - All actions become methods on the built object
- Returns the builder for chaining
Under the Hood
.actions({ inc(state){...}, dec(state){...} })
│
▼
┌───────────────────────┐
│ Receive Object │
│ { inc: fn, dec: fn } │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Loop Through Entries │
│ ['inc', fn] │
│ ['dec', fn] │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Create Methods │
│ state.inc() │
│ state.dec() │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Return Builder │
│ (for chaining) │
└───────────────────────┘What happens:
1️⃣ You pass an object with action definitions 2️⃣ Builder iterates through each property 3️⃣ For each action, creates a method on state 4️⃣ Same behavior as calling .action() multiple times 5️⃣ Returns builder for further chaining
Basic Usage
Adding Multiple Actions
The simplest way to use builder.actions():
// Create builder with multiple actions
const counter = reactive({ count: 0 })
.actions({
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
},
reset(state) {
state.count = 0;
}
})
.build();
counter.increment();
console.log(counter.count); // 1
counter.decrement();
console.log(counter.count); // 0
counter.reset();
console.log(counter.count); // 0Mixing with builder.action()
You can combine builder.actions() with individual builder.action() calls:
const counter = reactive({ count: 0 })
.actions({
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
}
})
.action('double', (state) => {
state.count *= 2;
})
.action('halve', (state) => {
state.count = Math.floor(state.count / 2);
})
.build();
counter.increment();
counter.double();
console.log(counter.count); // 2Multiple builder.actions() Calls
You can call builder.actions() multiple times:
const app = reactive({ count: 0, name: '' })
.actions({
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
}
})
.actions({
setName(state, newName) {
state.name = newName;
},
clearName(state) {
state.name = '';
}
})
.build();
app.increment();
app.setName('Counter');
console.log(app.count); // 1
console.log(app.name); // "Counter"Actions with Parameters
Single Parameter Actions
const counter = reactive({ count: 0 })
.actions({
add(state, amount) {
state.count += amount;
},
subtract(state, amount) {
state.count -= amount;
},
multiply(state, factor) {
state.count *= factor;
}
})
.build();
counter.add(5);
console.log(counter.count); // 5
counter.multiply(2);
console.log(counter.count); // 10Multiple Parameter Actions
const user = reactive({ firstName: '', lastName: '', age: 0 })
.actions({
setFullName(state, first, last) {
state.firstName = first;
state.lastName = last;
},
setProfile(state, first, last, age) {
state.firstName = first;
state.lastName = last;
state.age = age;
},
update(state, updates) {
Object.assign(state, updates);
}
})
.build();
user.setFullName('John', 'Doe');
console.log(user.firstName); // "John"
console.log(user.lastName); // "Doe"
user.setProfile('Jane', 'Smith', 25);
console.log(user.age); // 25Rest Parameters
const list = reactive({ items: [] })
.actions({
addItems(state, ...newItems) {
state.items.push(...newItems);
},
removeItems(state, ...itemsToRemove) {
state.items = state.items.filter(
item => !itemsToRemove.includes(item)
);
},
setItems(state, ...items) {
state.items = items;
}
})
.build();
list.addItems('A', 'B', 'C');
console.log(list.items); // ['A', 'B', 'C']
list.removeItems('B');
console.log(list.items); // ['A', 'C']Actions with Return Values
Returning Computed Values
const calculator = reactive({ a: 5, b: 3 })
.actions({
sum(state) {
return state.a + state.b;
},
product(state) {
return state.a * state.b;
},
average(state) {
return (state.a + state.b) / 2;
}
})
.build();
console.log(calculator.sum()); // 8
console.log(calculator.product()); // 15
console.log(calculator.average()); // 4Returning Status Objects
const form = reactive({ username: '', password: '', email: '' })
.actions({
validateUsername(state) {
if (state.username.length < 3) {
return { valid: false, error: 'Too short' };
}
return { valid: true };
},
validatePassword(state) {
if (state.password.length < 8) {
return { valid: false, error: 'Too short' };
}
return { valid: true };
},
validateAll(state) {
const usernameResult = form.validateUsername();
const passwordResult = form.validatePassword();
return {
valid: usernameResult.valid && passwordResult.valid,
errors: {
username: usernameResult.error,
password: passwordResult.error
}
};
}
})
.build();
form.username = 'jo';
form.password = '123';
const result = form.validateAll();
console.log(result.valid); // false
console.log(result.errors); // { username: 'Too short', password: 'Too short' }Returning Modified Data
const cart = reactive({ items: [], tax: 0.1 })
.actions({
calculateSubtotal(state) {
return state.items.reduce((sum, item) => sum + item.price, 0);
},
calculateTax(state) {
return cart.calculateSubtotal() * state.tax;
},
calculateTotal(state) {
return cart.calculateSubtotal() + cart.calculateTax();
},
getSummary(state) {
return {
subtotal: cart.calculateSubtotal(),
tax: cart.calculateTax(),
total: cart.calculateTotal()
};
}
})
.build();
cart.items = [
{ name: 'Item 1', price: 10 },
{ name: 'Item 2', price: 20 }
];
const summary = cart.getSummary();
console.log(summary);
// { subtotal: 30, tax: 3, total: 33 }Chaining with Other Methods
Combining with Computed
const counter = reactive({ count: 0 })
.computed({
doubled() {
return this.state.count * 2;
}
})
.actions({
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
}
})
.build();
counter.increment();
console.log(counter.count); // 1
console.log(counter.doubled); // 2Combining with Watch
const app = reactive({ count: 0, changes: 0 })
.watch({
count(newVal, oldVal) {
console.log(`Count: ${oldVal} → ${newVal}`);
this.state.changes++;
}
})
.actions({
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
},
reset(state) {
state.count = 0;
}
})
.build();
app.increment();
// Logs: "Count: 0 → 1"
console.log(app.changes); // 1Combining with Effects
const app = reactive({ count: 0 })
.effect(() => {
console.log('Count is:', app.state.count);
})
.actions({
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
}
})
.build();
// Logs: "Count is: 0"
app.increment();
// Logs: "Count is: 1"Full Chain Example
const app = reactive({ count: 0, history: [] })
.computed({
doubled() {
return this.state.count * 2;
},
historyLength() {
return this.state.history.length;
}
})
.watch({
count(newVal) {
this.state.history.push(newVal);
}
})
.effect(() => {
document.getElementById('count').textContent = app.state.count;
})
.actions({
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
},
reset(state) {
state.count = 0;
state.history = [];
},
setCount(state, value) {
state.count = value;
}
})
.build();
app.increment();
console.log(app.count); // 1
console.log(app.doubled); // 2
console.log(app.historyLength); // 1builder.actions() vs builder.action()
Both add actions, but differ in syntax:
When to Use builder.actions()
Use builder.actions() when adding multiple actions together:
✅ Multiple related actions ✅ Grouping actions by feature ✅ Prefer object syntax ✅ More concise for many actions
const obj = reactive({ count: 0 })
.actions({
increment(state) { state.count++; },
decrement(state) { state.count--; },
reset(state) { state.count = 0; }
})
.build();When to Use builder.action()
Use builder.action() when adding one action at a time:
✅ Adding actions incrementally ✅ Conditional action addition ✅ Clear, linear flow ✅ Prefer separate calls
const obj = reactive({ count: 0 })
.action('increment', (state) => state.count++)
.action('decrement', (state) => state.count--)
.action('reset', (state) => state.count = 0)
.build();Quick Comparison
// ✅ builder.actions() - Object syntax
const obj1 = reactive({ count: 0 })
.actions({
increment(state) { state.count++; },
decrement(state) { state.count--; },
reset(state) { state.count = 0; }
})
.build();
// ✅ builder.action() - Separate calls
const obj2 = reactive({ count: 0 })
.action('increment', (state) => state.count++)
.action('decrement', (state) => state.count--)
.action('reset', (state) => state.count = 0)
.build();
// Both produce identical results!Simple Rule:
- Multiple actions?
actions()is more concise - One action?
action()is clearer - Mixed? You can use both together
- Both produce the same result - choose based on preference
Common Patterns
Pattern: CRUD Operations
const todos = reactive({ list: [] })
.actions({
create(state, todo) {
state.list.push({ ...todo, id: Date.now() });
},
read(state, id) {
return state.list.find(t => t.id === id);
},
update(state, id, updates) {
const todo = state.list.find(t => t.id === id);
if (todo) Object.assign(todo, updates);
},
delete(state, id) {
state.list = state.list.filter(t => t.id !== id);
},
list(state) {
return state.list;
}
})
.build();
todos.create({ text: 'Learn Reactive', done: false });
const todo = todos.read(todos.list[0].id);
todos.update(todo.id, { done: true });
todos.delete(todo.id);Pattern: Form Actions
const form = reactive({
values: {},
errors: {},
touched: {},
submitted: false
})
.actions({
setValue(state, field, value) {
state.values[field] = value;
},
setError(state, field, error) {
state.errors[field] = error;
},
touch(state, field) {
state.touched[field] = true;
},
reset(state) {
state.values = {};
state.errors = {};
state.touched = {};
state.submitted = false;
},
submit(state) {
state.submitted = true;
}
})
.build();
form.setValue('email', 'test@example.com');
form.touch('email');
form.submit();Pattern: State Machine Actions
const machine = reactive({ state: 'idle', data: null, error: null })
.actions({
startLoading(state) {
state.state = 'loading';
state.data = null;
state.error = null;
},
loadSuccess(state, data) {
state.state = 'success';
state.data = data;
state.error = null;
},
loadError(state, error) {
state.state = 'error';
state.data = null;
state.error = error;
},
reset(state) {
state.state = 'idle';
state.data = null;
state.error = null;
}
})
.build();
machine.startLoading();
// ... fetch data ...
machine.loadSuccess({ items: [] });Pattern: Array Manipulation
const list = reactive({ items: [] })
.actions({
push(state, item) {
state.items.push(item);
},
pop(state) {
return state.items.pop();
},
shift(state) {
return state.items.shift();
},
unshift(state, item) {
state.items.unshift(item);
},
remove(state, index) {
state.items.splice(index, 1);
},
clear(state) {
state.items = [];
},
filter(state, predicate) {
state.items = state.items.filter(predicate);
},
map(state, mapper) {
state.items = state.items.map(mapper);
}
})
.build();
list.push('A');
list.push('B');
list.remove(0);
console.log(list.items); // ['B']Pattern: Async Operations
const api = reactive({ data: null, loading: false, error: null })
.actions({
async fetch(state, url) {
state.loading = true;
state.error = null;
try {
const response = await fetch(url);
const data = await response.json();
state.data = data;
} catch (err) {
state.error = err.message;
} finally {
state.loading = false;
}
},
async post(state, url, body) {
state.loading = true;
state.error = null;
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await response.json();
state.data = data;
} catch (err) {
state.error = err.message;
} finally {
state.loading = false;
}
},
reset(state) {
state.data = null;
state.loading = false;
state.error = null;
}
})
.build();
await api.fetch('/api/data');
console.log(api.data);Common Pitfalls
Pitfall #1: Forgetting state Parameter
❌ Wrong:
reactive({ count: 0 })
.actions({
increment() { // Missing state parameter!
this.count++; // 'this' is not the state!
}
})
.build();✅ Correct:
reactive({ count: 0 })
.actions({
increment(state) { // Include state parameter!
state.count++;
}
})
.build();Pitfall #2: Action Name Conflicts
❌ Wrong:
reactive({ count: 0 })
.actions({
count(state) { // 'count' already exists as property!
return state.count;
}
})
.build();This overwrites the count property.
✅ Correct:
reactive({ count: 0 })
.actions({
getCount(state) { // Use different name
return state.count;
}
})
.build();Pitfall #3: Using Arrow Functions with this
❌ Wrong:
reactive({ count: 0 })
.computed({
doubled() {
return this.state.count * 2;
}
})
.actions({
logDoubled: (state) => {
console.log(this.state.doubled); // 'this' won't work in arrow function!
}
})
.build();✅ Correct:
reactive({ count: 0 })
.computed({
doubled() {
return this.state.count * 2;
}
})
.actions({
logDoubled(state) { // Regular function
// Access via the built object reference
console.log(state.doubled);
}
})
.build();Pitfall #4: Not Returning When Needed
❌ Wrong:
reactive({ items: [] })
.actions({
getFirst(state) {
state.items[0]; // No return!
}
})
.build();
const first = obj.getFirst();
console.log(first); // undefined✅ Correct:
reactive({ items: [] })
.actions({
getFirst(state) {
return state.items[0]; // Return the value!
}
})
.build();
const first = obj.getFirst();
console.log(first); // Works!Pitfall #5: Duplicate Action Names
❌ Wrong:
reactive({ count: 0 })
.actions({
increment(state) {
state.count++;
}
})
.action('increment', (state) => { // Duplicate name!
state.count += 2;
})
.build();The second increment overwrites the first.
✅ Correct:
reactive({ count: 0 })
.actions({
increment(state) {
state.count++;
},
incrementBy2(state) { // Different name
state.count += 2;
}
})
.build();Summary
What is builder.actions()?
builder.actions() is a builder method that adds multiple named methods (actions) to your reactive state at once using an object.
Why use builder.actions()?
- Add multiple actions in one call
- Less repetitive syntax
- Clear grouping of related actions
- More concise for many actions
- Chainable with other builder methods
Key Points to Remember:
1️⃣ Object of actions - Pass an object with action definitions 2️⃣ Keys are names - Object keys become method names 3️⃣ Values are functions - Object values are action functions 4️⃣ State parameter - Each function receives state as first parameter 5️⃣ Returns builder - Chain with other methods
Mental Model: Think of builder.actions() as installing a complete control panel - all buttons added at once in one organized package!
Quick Reference:
// MULTIPLE ACTIONS
reactive({ count: 0 })
.actions({
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
},
reset(state) {
state.count = 0;
}
})
.build();
// WITH PARAMETERS
reactive({ count: 0 })
.actions({
add(state, amount) {
state.count += amount;
},
multiply(state, factor) {
state.count *= factor;
}
})
.build();
// WITH RETURN VALUES
reactive({ a: 5, b: 3 })
.actions({
sum(state) {
return state.a + state.b;
},
product(state) {
return state.a * state.b;
}
})
.build();
// MIX WITH OTHER METHODS
reactive({ count: 0 })
.computed({ doubled() { return this.state.count * 2; } })
.watch({ count(n) { console.log(n); } })
.actions({
increment(state) { state.count++; },
decrement(state) { state.count--; }
})
.build();
// USING THE ACTIONS
const counter = /* ... */.build();
counter.increment();
counter.add(5);
const sum = counter.sum();Remember: builder.actions() lets you add multiple actions at once in a clean, organized way. It's the same as calling builder.action() multiple times, just more concise!