Reactivity

Courvux uses Proxy-based reactivity. Every key in data is observable — reading it creates a subscription, writing it notifies subscribers.

Computed properties

Automatically recalculate when their dependencies change. Dependencies are detected by parsing this.key references in the getter source.

JavaScript
{
    data: { price: 10, qty: 3 },
    computed: {
        total() { return this.price * this.qty; }
    },
    template: `<p>Total: {{ total }}</p>`
}

Computed setter

JavaScript
computed: {
    fullName: {
        get() { return `${this.first} ${this.last}`.trim(); },
        set(val) {
            const [f, ...rest] = val.split(' ');
            this.first = f ?? '';
            this.last  = rest.join(' ');
        }
    }
}

Watchers

React to state changes. Receives (newVal, oldVal) with this bound to component state.

JavaScript
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); }
    }
}

Programmatic watcher — $watch

JavaScript
onMount() {
    // Returns an unsubscribe function
    const stop = this.$watch('count', (newVal, oldVal) => {
        console.log(oldVal, '→', newVal);
    }, { immediate: true });

    // Stop later:
    // stop();
}

$batch — group mutations

Multiple state changes inside $batch trigger only one DOM update cycle.

JavaScript
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';
});

$nextTick — after DOM update

Runs a callback after the next reactive flush. Also returns a Promise.

JavaScript
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');
    }
}

$watchEffect — auto-tracked effect

Runs immediately and re-runs when any reactive key accessed inside it changes. Stopped automatically on component destroy.

JavaScript
onMount() {
    // Auto-tracked: re-runs when any accessed reactive key changes
    this.$watchEffect(() => {
        document.title = `${this.count} items — MyApp`;
    });
    // Stopped automatically on component destroy
}

Escape hatches

Three helpers let you opt out of reactivity selectively:

JavaScript
import { markRaw, toRaw, readonly } from 'courvux';
HelperUse 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)

markRaw

JavaScript
// 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
    }
}

toRaw

JavaScript
// 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));

readonly

JavaScript
// Wrap so writes are silently ignored (with a warning).
// Use for provide values that descendants must not mutate.
provide() {
    return {
        config: readonly(this.appConfig),
    };
}
Native built-ins like Date, Map, Set, RegExp, and typed arrays are automatically skipped from Proxy wrapping — you don't need markRaw for them.

Common gotchas

Proxy identity — prefer findIndex over indexOf

Courvux 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:

JavaScript
// ❌ 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

Hot-path mutations — wrap in $batch

If 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.

JavaScript
// 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);
});
Since 0.5.1, 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.
← Components Lifecycle →