Specialized State Factories
The reactive module provides purpose-built factories for common patterns. Instead of manually setting up state + computed + methods every time, these give you a ready-to-use reactive object for a specific use case.
Quick comparison
| Factory | What it creates | Built-in properties | Built-in methods |
|---|---|---|---|
ref() | Single value wrapper | .value | .valueOf(), .toString() |
collection() | Reactive list | .items | add, remove, update, clear |
form() | Form state | .values, .errors, .touched, .isSubmitting | setValue, setError, reset |
async() | Async operation state | .data, .loading, .error | execute, reset |
ref() — Single reactive value
What is it?
ref() wraps a single value in a reactive container. The value is accessed through .value.
Syntax
const count = ref(0);
console.log(count.value); // 0
count.value = 5; // Triggers effectsWhy use it?
Reactive state requires an object. When you only need a single value, ref() provides a clean wrapper:
// Without ref — need an object with a named property
const state = state({ count: 0 });
state.count++;
// With ref — clean single-value container
const count = ref(0);
count.value++;With effects
const count = ref(0);
effect(() => {
Elements.counter.update({ textContent: count.value });
});
count.value = 42; // Counter updates automaticallyvalueOf() and toString()
ref() adds .valueOf() and .toString() so the ref can be used in expressions:
const count = ref(10);
console.log(count + 5); // 15 (valueOf returns count.value)
console.log(`Count: ${count}`); // "Count: 10" (toString returns String(count.value))Creating multiple refs
const refs = refs({
count: 0,
name: 'Alice',
isActive: true
});
console.log(refs.count.value); // 0
console.log(refs.name.value); // 'Alice'
console.log(refs.isActive.value); // true
refs.count.value = 5; // Triggers effects watching refs.countcollection() — Reactive lists
What is it?
collection() creates a reactive list with built-in methods for adding, removing, updating, and clearing items.
Syntax
const todos = collection([
{ id: 1, text: 'Learn Reactive', done: false },
{ id: 2, text: 'Build app', done: false }
]);The items property
Items are stored in .items:
console.log(todos.items); // [{ id: 1, ... }, { id: 2, ... }]
console.log(todos.items.length); // 2
console.log(todos.items[0].text); // 'Learn Reactive'add() — Add an item
todos.add({ id: 3, text: 'Deploy', done: false });
console.log(todos.items.length); // 3remove() — Remove an item
By reference:
const item = todos.items[0];
todos.remove(item);By predicate function:
todos.remove(item => item.id === 2);update() — Update an item
By reference:
const item = todos.items[0];
todos.update(item, { done: true });By predicate function:
todos.update(
item => item.id === 1,
{ done: true, text: 'Learn Reactive (done!)' }
);clear() — Remove all items
todos.clear();
console.log(todos.items.length); // 0Complete example
const todos = collection();
// Add items
todos.add({ id: 1, text: 'Buy groceries', done: false });
todos.add({ id: 2, text: 'Clean house', done: false });
todos.add({ id: 3, text: 'Walk dog', done: false });
// Mark one as done
todos.update(item => item.id === 2, { done: true });
// Remove completed
todos.remove(item => item.done);
// Clear all
todos.clear();Also available as list()
const items = ReactiveUtils.list(); // Same as collection()form() — Form state management
What is it?
form() creates a reactive state pre-configured for form handling — with values, errors, touched fields, and validation tracking.
Syntax
const loginForm = form({
email: '',
password: ''
});What you get
console.log(loginForm.values); // { email: '', password: '' }
console.log(loginForm.errors); // {}
console.log(loginForm.touched); // {}
console.log(loginForm.isSubmitting); // false
console.log(loginForm.isValid); // true (computed — no errors)
console.log(loginForm.isDirty); // false (computed — no touched fields)setValue() — Set a field value
loginForm.setValue('email', 'alice@example.com');
console.log(loginForm.values.email); // 'alice@example.com'
console.log(loginForm.touched.email); // true (auto-marked)
console.log(loginForm.isDirty); // true (has touched fields)setError() — Set or clear an error
// Set an error
loginForm.setError('email', 'Invalid email address');
console.log(loginForm.errors.email); // 'Invalid email address'
console.log(loginForm.isValid); // false
// Clear an error
loginForm.setError('email', null);
console.log(loginForm.errors.email); // undefined
console.log(loginForm.isValid); // truereset() — Reset the form
// Reset to initial values
loginForm.reset();
console.log(loginForm.values); // { email: '', password: '' }
console.log(loginForm.errors); // {}
console.log(loginForm.touched); // {}
// Reset to new values
loginForm.reset({ email: 'new@example.com', password: '' });Computed properties
The form comes with two computed properties:
- isValid —
truewhen there are no errors (or all errors are falsy) - isDirty —
truewhen at least one field has been touched
effect(() => {
Elements.submitBtn.update({ disabled: !loginForm.isValid });
});Complete form example
const signupForm = form({
name: '',
email: '',
password: ''
});
// Validation
function validate() {
if (!signupForm.values.name) {
signupForm.setError('name', 'Name is required');
} else {
signupForm.setError('name', null);
}
if (!signupForm.values.email.includes('@')) {
signupForm.setError('email', 'Invalid email');
} else {
signupForm.setError('email', null);
}
if (signupForm.values.password.length < 8) {
signupForm.setError('password', 'Must be 8+ characters');
} else {
signupForm.setError('password', null);
}
}
// React to validation state
effect(() => {
Elements.submitBtn.update({ disabled: !signupForm.isValid || signupForm.isSubmitting });
});async() — Async operation state
What is it?
async() creates a reactive state for managing asynchronous operations — with loading, error, and data tracking built in.
Syntax
const users = async();
// Or with an initial value:
const users = async([]);What you get
console.log(users.data); // null (or your initial value)
console.log(users.loading); // false
console.log(users.error); // null
console.log(users.isSuccess); // false (computed)
console.log(users.isError); // false (computed)execute() — Run an async operation
await users.execute(async () => {
const response = await fetch('/api/users');
return response.json();
});
console.log(users.data); // [{ id: 1, name: 'Alice' }, ...]
console.log(users.loading); // false
console.log(users.isSuccess); // trueWhat execute() does automatically
users.execute(asyncFn)
↓
1️⃣ Set loading = true
↓
2️⃣ Set error = null
↓
3️⃣ Run the async function
├── Success → data = result
└── Error → error = exception (re-throws)
↓
4️⃣ Set loading = false (always, via finally)reset() — Reset to initial state
users.reset();
console.log(users.data); // null (or initial value)
console.log(users.loading); // false
console.log(users.error); // nullComputed properties
- isSuccess —
truewhen not loading, no error, and data is not null - isError —
truewhen not loading and error is not null
Complete async example
const userList = async([]);
// Keep UI in sync with loading state
effect(() => {
Elements.update({
spinner: { hidden: !userList.loading },
content: { hidden: !userList.isSuccess },
error: { hidden: !userList.isError, textContent: userList.error?.message || '' }
});
});
// Fetch users
Elements.loadBtn.addEventListener('click', async () => {
try {
await userList.execute(async () => {
const res = await fetch('/api/users');
if (!res.ok) throw new Error('Failed to load');
return res.json();
});
} catch (e) {
console.log('Failed:', e.message);
}
});
// Retry button
Elements.retryBtn.addEventListener('click', () => {
userList.reset();
});Key takeaways
- ref() — single reactive value with
.value, good for counters, flags, selections - collection() — reactive list with
add,remove,update,clear - form() — form state with values, errors, touched, computed
isValidandisDirty - async() — async state with data, loading, error, computed
isSuccessandisError - All factories return reactive objects — they work with effects, watchers, and computed
- Each factory provides purpose-built methods so you don't have to write boilerplate
What's next?
Now let's explore the higher-level architecture tools:
store()for state + getters + actionscomponent()for full component definitionreactive()for the chainable builder pattern
Let's continue!