Skip to content

9.9. Vue 3 Integration

Wire a ComponentVMOf<M> to a Vue 3 component via the Composition API reactive() / ref() primitives.

9.9.1. Reactivity primitive

Vue 3's reactivity tracks reads and writes to reactive(obj) and ref() values; templates re-render automatically when tracked values change. Bridge VMx by syncing a local ref from VMx hub events.

9.9.2. Mapping

Vue 3 VMx
shallowRef value + triggerRef PropertyChangedMessage<T> handler
@click="fn" command.execute() in fn
computed(() => ...) DerivedProperty<T> / fromSources
onUnmounted(() => cleanup) dispose the subscription

republishModel() announces a model that was mutated in place, so the announced value is the same object the binding already holds. A plain ref ignores assigning the same reference, so the composable stores the value in a shallowRef and calls triggerRef on every matching message. A replacement object or a scalar value re-renders the same way, and messages for another VM or another property are filtered out.

9.9.3. Adapter skeleton

The composable and component below are the exact files run by examples/typescript/integration-recipes against vue 3.5.43, including an in-place mutation announced with republishModel(), replacement and scalar updates, unrelated messages, and unmount cleanup.

// composables/useVm.ts
import { onUnmounted, shallowRef, triggerRef, type ShallowRef } from "vue";
import { filter } from "rxjs";
import { type ComponentVMOf, type IMessageHub, PropertyChangedMessage } from "@thekaveh/vmx";

/**
 * Binds one VM property for the calling component's lifetime. `vm` and `hub`
 * are fixed inputs: give the component a `:key` to rebind it to another VM.
 */
export function useVm<M, K extends keyof ComponentVMOf<M>>(
  vm: ComponentVMOf<M>,
  hub: IMessageHub,
  property: K,
): Readonly<ShallowRef<ComponentVMOf<M>[K]>> {
  const value = shallowRef(vm[property]) as ShallowRef<ComponentVMOf<M>[K]>;
  const sub = hub.messages
    .pipe(
      filter(
        (m): m is PropertyChangedMessage<unknown> =>
          m instanceof PropertyChangedMessage && m.sender === vm && m.propertyName === property,
      ),
    )
    .subscribe(() => {
      value.value = vm[property];
      // A republished model keeps its reference, so trigger explicitly.
      triggerRef(value);
    });
  onUnmounted(() => sub.unsubscribe());
  return value;
}
<script setup lang="ts">
import type { ComponentVMOf, ICommand, IMessageHub } from "@thekaveh/vmx";
import { useVm } from "./composables/useVm";
import type { Note } from "./note";

const props = defineProps<{
  vm: ComponentVMOf<Note>;
  hub: IMessageHub;
  saveCommand: ICommand;
}>();
const model = useVm(props.vm, props.hub, "model");
</script>

<template>
  <h1>{{ model.title }}</h1>
  <button @click="props.saveCommand.execute()">Save</button>
</template>

vm and hub are fixed for the binding's lifetime. To show a different VM, re-create the component with a :key that identifies the VM, such as :key="vm.name" when sibling names are unique.

9.9.4. Fuller example

No worked Vue Notes-Showcase ships yet. The React recipe (react.md) uses the same hub-subscription shape (just with useSyncExternalStore instead of ref) and is a good reference.