Courvux uses Proxy-based reactivity. Every key in data is observable — reading it creates a subscription, writing it notifies subscribers.
Automatically recalculate when their dependencies change. Dependencies are detected by parsing this.key references in the getter source.
{
data: { price: 10, qty: 3 },
computed: {
total() { return this.price * this.qty; }
},
template: `<p>Total: {{ total }}</p>`
}
computed: {
fullName: {
get() { return `${this.first} ${this.last}`.trim(); },
set(val) {
const [f, ...rest] = val.split(' ');
this.first = f ?? '';
this.last = rest.join(' ');
}
}
}
React to state changes. Receives (newVal, oldVal) with this bound to component state.
watch: {
// Simple watcher
search(newVal, oldVal) {
if (newVal) this.fetchResults(newVal);
},
// With options
count: {
immediate: true, // run once on mount with current value
handler(newVal, oldVal) {
this.log.push(`${oldVal ?? 'init'} → ${newVal}`);
}
},
// Deep — detects nested mutations inside objects/arrays
user: {
deep: true,
handler(newVal) { console.log('user changed:', newVal); }
}
}
onMount() {
// Returns an unsubscribe function
const stop = this.$watch('count', (newVal, oldVal) => {
console.log(oldVal, '→', newVal);
}, { immediate: true });
// Stop later:
// stop();
}
Multiple state changes inside $batch trigger only one DOM update cycle.
methods: {
updateAll() {
// One DOM flush instead of three
this.$batch(() => {
this.a++;
this.b++;
this.c = 'new';
});
}
}
// Named export — useful outside components
import { batchUpdate } from 'courvux';
batchUpdate(() => {
store.counter.n = 10;
store.user.role = 'admin';
});
Runs a callback after the next reactive flush. Also returns a Promise.
methods: {
addItem() {
this.items.push({ id: Date.now(), text: 'New' });
// DOM not yet updated — wait for next flush
this.$nextTick(() => {
this.$refs.list.lastElementChild?.scrollIntoView();
});
},
// Also returns a Promise
async save() {
this.saved = true;
await this.$nextTick();
console.log('DOM updated, badge is visible');
}
}
Runs immediately and re-runs when any reactive key accessed inside it changes. Stopped automatically on component destroy.
onMount() {
// Auto-tracked: re-runs when any accessed reactive key changes
this.$watchEffect(() => {
document.title = `${this.count} items — MyApp`;
});
// Stopped automatically on component destroy
}
Three helpers let you opt out of reactivity selectively:
import { markRaw, toRaw, readonly } from 'courvux';
| Helper | Use case |
|---|---|
markRaw(obj) | Skip Proxy wrapping (third-party class instances like Chart.js or xterm.js controllers) |
toRaw(reactive) | Get the underlying non-Proxy object (serialization, JSON.stringify, deep equality) |
readonly(obj) | Wrap so writes are silently ignored (use for provide values that shouldn't mutate downstream) |
// Skip Proxy wrapping for third-party class instances whose internal
// slots break under Proxy (Chart.js, xterm.js, Map, Set, etc.)
{
data: {
chart: markRaw(new Chart(canvas, opts)), // not made reactive
}
}
// Get the underlying non-Proxy object — useful for serialization,
// JSON.stringify, deep equality, or passing to non-reactive APIs.
const snapshot = toRaw(this.user);
console.log(JSON.stringify(snapshot));
// Wrap so writes are silently ignored (with a warning).
// Use for provide values that descendants must not mutate.
provide() {
return {
config: readonly(this.appConfig),
};
}
Date, Map, Set, RegExp, and typed arrays are automatically skipped from Proxy wrapping — you don't need markRaw for them.
findIndex over indexOfCourvux wraps each property access in a fresh Proxy, so the object you get from .find() is not === to the same row when read again from the array. indexOf(proxy) returns -1, and splice(-1, 1) silently deletes the last row instead of the one you meant. Look items up by their primitive id with findIndex:
// ❌ Pitfall — proxy identity is per-access
const card = this.cards.find(c => c.id === this.dragId);
const idx = this.cards.indexOf(card); // -1 — different proxy wrapper!
this.cards.splice(idx, 1); // splice(-1, 1) deletes the LAST row
// ✅ Always look items up by primitive id, never by proxy reference
const idx = this.cards.findIndex(c => c.id === this.dragId);
this.cards.splice(idx, 1);
// ✅ Or unwrap with toRaw if you really need identity
import { toRaw } from 'courvux';
const raw = toRaw(card);
this.cards.indexOf(raw); // works, but findIndex is usually clearer
$batchIf you do three or more rapid mutations to the same array (drag-and-drop, bulk edits, etc.), each one schedules its own re-render. Wrapping the sequence in $batch coalesces them into a single render with the final state — usually a noticeable speedup, and it sidesteps any chance of intermediate states being painted.
// Three rapid mutations to the same array key — drop handler in a kanban
// board, swap rows in a table, etc. Each one schedules its own re-render.
// Wrap in $batch so cv-for re-renders once with the final state instead of
// three times.
this.$batch(() => {
this.rows[fromIdx].col = toCol;
const [moved] = this.rows.splice(fromIdx, 1);
this.rows.push(moved);
});
cv-for with :key serializes overlapping renders internally, so even without $batch the DOM stays correct. $batch is now a pure performance lever for these patterns, not a correctness requirement.