@strator/vue
Native Vue 3 adapter for Strator MVVM models. Bridges pure TypeScript domain logic with Vue's reactive() proxies and Composition API for automatic dependency tracking and zero boilerplate.
Getting Started with Vue 3
Install the plugin and start using Strator models with Vue Composition API in 3 simple steps.
Register the createStrator() Plugin
Install createStrator() into your Vue app instance in main.ts.
import { createApp } from "vue";
import { createStrator } from "@strator/vue";
import App from "./App.vue";
const app = createApp(App);
app.use(createStrator());
app.mount("#app");Define Pure Domain Model
Pure TypeScript class inheriting from Model<T>. Exactly identical across React, Vue, or Node.js!
import { Model } from "@strator/core";
export interface CounterState {
count: number;
}
export class CounterModel extends Model<CounterState> {
static initialState: CounterState = { count: 0 };
public increase() {
this.state.count += 1;
}
public reset() {
this.state.count = 0;
}
}Use in <script setup>
Consume models with useLocalModel. Automatic cleanup on unmount!
<script setup lang="ts">
import { useLocalModel } from "@strator/vue";
import { CounterModel } from "./CounterModel";
// Dual syntax: [model, count] or { model, state: count }
const [model, count] = useLocalModel(CounterModel, s => s.count);
</script>
<template>
<div class="p-6 bg-slate-900 rounded-xl border border-white/10 space-y-4">
<h2 class="text-xl font-bold text-white">Count: {{ count }}</h2>
<div class="flex gap-2">
<button
@click="model.increase()"
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 rounded-lg text-white font-mono"
>
+1
</button>
<button
@click="model.reset()"
class="px-4 py-2 bg-slate-800 hover:bg-slate-700 rounded-lg text-white font-mono"
>
Reset
</button>
</div>
</div>
</template>@strator/vue API Reference
Explore all plugin options, composables, and components in @strator/vue.
Standard Vue 3 plugin that installs Strator's root context and model registry into the application using app.use().
Signature
createStrator(options?: { initialState?: Record<string, any> | Map<string, any> }): PluginParameters
| Parameter | Type | Description |
|---|---|---|
| options.initialState(optional) | Record<string, any> | Map<string, any> | Initial state object or Map for hydrating models during SSR or initialization. |
Returns
- Install once in your main.ts before mounting the Vue app.
- Automatically makes all Strator composables available throughout the Vue component hierarchy.
Example
import { createApp } from "vue";
import { createStrator } from "@strator/vue";
import App from "./App.vue";
const app = createApp(App);
app.use(createStrator());
app.mount("#app");Component or helper function for creating scoped Strator context boundaries in isolated subtrees or micro-frontends without registering globally.
Signature
<StratorProvider :initial-state='initialState'><slot /></StratorProvider>Parameters
| Parameter | Type | Description |
|---|---|---|
| initialState(optional) | Record<string, any> | Map<string, any> | Initial state mapping for this scoped context boundary. |
Returns
- Use when embedding isolated widgets or test fixtures that must not share state with the global app.
Example
<template>
<StratorProvider :initial-state="{ 'CartModel': { items: [] } }">
<CheckoutSubtree />
</StratorProvider>
</template>
<script setup lang="ts">
import { StratorProvider } from "@strator/vue";
import CheckoutSubtree from "./CheckoutSubtree.vue";
</script>Vue Reactivity & syncState Architecture
How @strator/vue synchronizes pure JavaScript class instances with Vue 3's reactive proxy engine.
Plugin & Context Initialization
Global STRATOR_CONTEXT_KEY Provisioning
When app.use(createStrator()) runs, it configures a VueDispatcher and Model instance cache, providing STRATOR_CONTEXT_KEY to Vue's injection system.
Strator eliminates unnecessary re-renders by enforcing clean boundaries between data manipulation and UI notifications.
// main.ts
const app = createApp(App);
app.use(createStrator());
app.mount("#app");Native Vue Dependency Tracking
Unlike React which requires forced ticks to re-render, Vue tracks accessed properties directly through the reactive proxy. If a template only reads user.name, modifying user.email triggers zero template updates.
Automatic onScopeDispose Lifecycle
Local models created with useLocalModel automatically clean themselves up when their parent component's Vue EffectScope is destroyed.
Dual Return Syntax
All composables return a UseModelResult that can be destructured as an array [model, state] or as an object { model, state }, maximizing flexibility.
Real-World Vue 3 Examples
Practical patterns for global singletons, shared state, and full reactive state access.
Full Reactive State Access
Direct ProxyWhen selector is omitted in Vue, you receive the full reactive state proxy with direct dot-notation access.
<script setup lang="ts">
import { useGlobalModel } from "@strator/vue";
import { UserModel } from "./UserModel";
// Omit selector: state is the full reactive proxy
const [userModel, state] = useGlobalModel(UserModel);
</script>
<template>
<div class="user-card">
<h3>{{ state.profile.name }}</h3>
<p>{{ state.profile.email }}</p>
<button @click="userModel.updateStatus('active')">Set Active</button>
</div>
</template>Shared Shopping Cart
useSharedModelSeamlessly share state across multiple Vue components with fine-grained computed selectors.
<script setup lang="ts">
import { useSharedModel } from "@strator/vue";
import { CartModel } from "./CartModel";
// Select only item count for the header badge
const [cart, count] = useSharedModel("main-cart", CartModel, s => s.items.length);
</script>
<template>
<div class="badge">
Cart Items: {{ count }}
</div>
</template>Current Limitations & Best Practices
To make optimal architectural decisions, understand how the @strator/vue adapter behaves under the hood, where edge cases exist, and how to write reliable, memory-efficient code.
Deep State Sync Overhead on Massive Collections
syncState recursively traverses and synchronizes object properties and array items into the reactive proxy on state mutation notifications.
To keep Vue's reactive proxy in sync with the model's internal plain JS state, syncState compares keys and array elements recursively. For very large collections (e.g. arrays of 50,000+ items modified frequently), this recursive sync can introduce CPU overhead during high-frequency mutations.
Impact: Performance degradation during bulk modifications of huge, deeply nested arrays or graphs.
For massive collections, paginate or normalize lists into maps keyed by IDs, or slice data at the Model level before storing in state.
// Massive monolithic array in state
export class TableModel extends Model<{ rows: LargeItem[] }> {
public updateAll() {
this.state.rows = 100_000_items; // syncState traverses all 100k items
}
}// Normalized or paginated state
export class TableModel extends Model<{ page: LargeItem[]; total: number }> {
public setPage(pageItems: LargeItem[]) {
this.state.page = pageItems; // Fast 20-50 item sync
}
}ComputedRef .value Unwrapping Behavior
When a selector function is passed to useLocalModel/useModel, the returned state is a Vue ComputedRef, requiring .value in <script setup>.
In Vue 3 templates, ComputedRef values are automatically unwrapped (e.g., {{ count }}). However, inside <script setup lang='ts'>, you must access count.value if you perform computations or logging in script code. Conversely, when no selector is passed, state is a reactive proxy where properties are accessed directly without .value.
Impact: Developers accustomed to React hooks might forget .value in Vue script setup, resulting in referencing the ref object rather than the primitive value.
Remember that selectors produce a ComputedRef<S>. Use count.value in script setup and {{ count }} in templates.
<script setup lang="ts">
const [model, count] = useLocalModel(CounterModel, s => s.count);
// ❌ Bug: count is a ComputedRef, cannot use math operators directly
console.log(count + 1); // "[object Object]1"
</script><script setup lang="ts">
const [model, count] = useLocalModel(CounterModel, s => s.count);
// ✅ Correct: access .value in script setup
console.log(count.value + 1);
</script>
<template>
<!-- ✅ In template, automatically unwrapped -->
<div>{{ count }}</div>
</template>Direct Reactive Proxy Mutation Divergence
Mutating the returned reactive proxy directly (state.count++) bypasses Model class methods and breaks encapsulation.
In Vue, modifying a reactive proxy (state.count++) triggers Vue template updates. However, doing so does NOT invoke Model action validation, logging, or core dispatchers, and can cause the Model's private internal state to diverge from Vue's proxy until the next class method executes.
Impact: Bypasses business logic invariants and breaks zero-mock unit test guarantees.
Always trigger mutations exclusively through Model class methods (model.increase()) rather than mutating state directly.
<!-- ❌ Anti-pattern: direct proxy mutation -->
<button @click="state.count++">Increment</button><!-- ✅ Recommended: call model class method -->
<button @click="model.increase()">Increment</button>Vue Injection Context Requirement
Strator composables must be invoked during Vue component setup() or inside an active effect scope.
Composables like useLocalModel rely on inject(STRATOR_CONTEXT_KEY) and onScopeDispose. Calling them outside of setup() or an active injection context will cause them to throw an error indicating context is unavailable.
Impact: Fails if called inside asynchronous callbacks after setup has finished.
Always call useLocalModel, useSharedModel, and useGlobalModel at the top level of <script setup>.
<script setup lang="ts">
// ✅ Called synchronously in setup()
const [auth, user] = useGlobalModel(AuthModel, s => s.currentUser);
</script>Looking for React bindings?
Explore the React 18 & 19 guide with selective hooks, shallowEqual diffing, and SSR hydration.