Vue Bindings|v100.1.0

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

Installation
$pnpm add @strator/core @strator/vue
Vue 3 Setup

Getting Started with Vue 3

Install the plugin and start using Strator models with Vue Composition API in 3 simple steps.

1

Register the createStrator() Plugin

Install createStrator() into your Vue app instance in main.ts.

main.ts
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");
2

Define Pure Domain Model

Pure TypeScript class inheriting from Model<T>. Exactly identical across React, Vue, or Node.js!

CounterModel.ts
ts
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;
  }
}
3

Use in <script setup>

Consume models with useLocalModel. Automatic cleanup on unmount!

Counter.vue
vue
<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>
API Specification

@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> }): Plugin

Parameters

ParameterTypeDescription
options.initialState(optional)Record<string, any> | Map<string, any>Initial state object or Map for hydrating models during SSR or initialization.

Returns

Plugin
Vue 3 plugin instance providing STRATOR_CONTEXT_KEY to the entire app.
Usage Insights & Best Practices
  • 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

ParameterTypeDescription
initialState(optional)Record<string, any> | Map<string, any>Initial state mapping for this scoped context boundary.

Returns

StratorContext
Scoped context with independent model registry and dispatcher.
Usage Insights & Best Practices
  • 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>
Under The Hood

Vue Reactivity & syncState Architecture

How @strator/vue synchronizes pure JavaScript class instances with Vue 3's reactive proxy engine.

1

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.

Reconciliation Mechanics

Strator eliminates unnecessary re-renders by enforcing clean boundaries between data manipulation and UI notifications.

Implementation Sample
typescript
// 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.

Vue Composition API Patterns

Real-World Vue 3 Examples

Practical patterns for global singletons, shared state, and full reactive state access.

Full Reactive State Access

Direct Proxy

When selector is omitted in Vue, you receive the full reactive state proxy with direct dot-notation access.

ProfileCard.vue
vue
<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

useSharedModel

Seamlessly share state across multiple Vue components with fine-grained computed selectors.

CartBadge.vue
vue
<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>
Honest Engineering & Gotchas

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.

Important Gotcha
Technical Cause & Impact

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.

Recommended Mitigation

For massive collections, paginate or normalize lists into maps keyed by IDs, or slice data at the Model level before storing in state.

Avoid (Pitfall)
// 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
  }
}
Recommended Pattern
// 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>.

Design Trade-off
Technical Cause & Impact

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.

Recommended Mitigation

Remember that selectors produce a ComputedRef<S>. Use count.value in script setup and {{ count }} in templates.

Avoid (Pitfall)
<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>
Recommended Pattern
<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.

Critical Behavior
Technical Cause & Impact

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.

Recommended Mitigation

Always trigger mutations exclusively through Model class methods (model.increase()) rather than mutating state directly.

Avoid (Pitfall)
<!-- ❌ Anti-pattern: direct proxy mutation -->
<button @click="state.count++">Increment</button>
Recommended Pattern
<!-- ✅ 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.

Important Gotcha
Technical Cause & Impact

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.

Recommended Mitigation

Always call useLocalModel, useSharedModel, and useGlobalModel at the top level of <script setup>.

Recommended Pattern
<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.

Explore React Bindings