Skip to content

Getting Started ​

Install ​

sh
npm install ivue

ivue has no runtime dependencies of its own and supports Vue 3.2 or newer. Vue 3.2 is the minimum because $watch uses Vue's detached effectScope() to keep component-outliving model watchers under the model's ownership.

Give your AI the standard ​

If AI agents write code in your project, hand them the same manual this site publishes as The Standard — one command, and the copy is version-locked to your installed ivue:

sh
npx ivue skill        # Claude Code
npx ivue skill --all  # + Codex/Cursor/Copilot where already in use

It lands in your repo (.claude/skills/ivue/, .cursor/rules/, …), travels through git, and one teammate installing it equips everyone.

Development needs no ivue plugin ​

Use Vite's normal Vue plugin. ivue runs the same engine path in development and production:

ts
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
});

Vite and Vue reconstruct the affected component after a class or script edit, so constructor wiring, cached state, methods, inheritance, and private fields all come from one class generation. Template-only edits retain Vue's normal state-preserving HMR. See Development & HMR.

Your first reactive class ​

Write a normal class. Declare state as getters returning ref(); derive with plain getters. Wrap it with Reactive().

ts
// Counter.ts
import { Reactive } from 'ivue'
import { ref } from 'vue'

class $Counter {
  get count() {
    return ref(0)
  }
  get double() {
    return this.count.value * 2 // derived: plain getter
  }

  increment() {
    this.count.value++
  }
  reset() {
    this.count.value = 0
  }
}

export namespace Counter {
  export const $Class = $Counter // raw — children `extends` this
  export let Class = Reactive($Class) // reactive, replaceable — you `new` this
  export type Instance = typeof Class.Instance // defineExpose type & reactive() interop
}

Note what double is not: a computed(). Simple derivations stay plain getters. They re-derive whenever a render reads them, cost zero bytes per instance, and stay fully reactive. Reach for computed() only when the work is expensive or you need render suppression. See Computed & Watch.

The $-prefixed raw class (Counter.$Class) is what children extends; the transformed class (Counter.Class) is what you new. This three-line namespace export keeps the class ready for inheritance, cross-file references, and later kernel composition as the codebase grows.

Use it in a component ​

vue
<script setup lang="ts">
import { Counter } from './Counter'

const counter = new Counter.Class()

// destructure every Ref/Computed the template touches
const { count } = counter
</script>

<template>
  <button @click="counter.increment">{{ count }} → {{ counter.double }}</button>
  <button @click="counter.reset">reset</button>
</template>

That's it — count is the real ref, destructured straight off the instance (a state binding: unwrapped in the template, state at a glance); counter.double re-derives on every render; counter.increment is a stable bound method. The component re-renders when count changes, exactly as if you'd written refs by hand.

Result ​

The page below imports the same Counter class shown above:

Your first class, runningLive · runs the shipped engine
count
0
double · plain getter
0
double is a plain getter, not a computed(). It re-derives whenever the component renders.

What just happened ​

  • new Counter.Class() created a plain object — no proxy.
  • The first time you read count, its ref(0) was created and cached — the destructured binding IS that cached ref.
  • counter.double is a plain getter — dotted in the template, because dotted means derivation. The render effect reads it, subscribes to count underneath, and re-derives on change. No allocation.
  • counter.increment was bound to the instance once and reused.

Read the Fundamental Principles for the full picture, or jump to Reactive State.

Released under the MIT License.