Skip to main content

Framework Adapters

11 min read

Vue Adapter

Directive provides first-class Vue 3 integration with composables that automatically update on state changes. All composables take an explicit system parameter – no context injection needed.


Installation

The Vue adapter is included in the main package:

import { useFact, useDerived, useEvents, useDispatch } from '@directive-run/vue';

Setup

Create a system at module level and pass it explicitly to composables:

import { createModule, createSystem, t } from '@directive-run/core';

const userModule = createModule("user", {
  schema: {
    facts: {
      userId: t.number(),
      user: t.object<User | null>(),
    },
    derivations: {
      displayName: t.string(),
    },
    events: {
      setUserId: { userId: t.number() },
    },
    requirements: {
      FETCH_USER: {},
    },
  },
  init: (facts) => {
    facts.userId = 0;
    facts.user = null;
  },
  derive: {
    displayName: (facts) => facts.user?.name ?? "Guest",
  },
  constraints: {
    needsUser: {
      when: (facts) => facts.userId > 0 && !facts.user,
      require: { type: "FETCH_USER" },
    },
  },
  resolvers: {
    fetchUser: {
      requirement: "FETCH_USER",
      resolve: async (req, context) => {
        context.facts.user = await api.getUser(context.facts.userId);
      },
    },
  },
});

// Create and start the system
const system = createSystem({ module: userModule });
system.start();

// Export for use in components
export { system };

Then pass the system to composables in your components:

<script setup>
import { useFact, useDerived } from '@directive-run/vue';
import { system } from './system';

const user = useFact(system, 'user');
const displayName = useDerived(system, 'displayName');
</script>

<template>
  <h1>{{ displayName }}</h1>
</template>

Creating Systems

Every composable below requires a system passed as the first parameter. There are two ways to create one:

  • Global system – call createSystem() at module level for app-wide state shared across components (shown in Setup above)
  • useDirective – creates a system scoped to a component's lifecycle, auto-starts on mount and destroys on unmount

For most Vue apps, use a global system. Use useDirective when you need per-component system isolation.

useDirective

Creates a scoped system and subscribes to facts and derivations. Two modes:

  • Selective – specify facts and/or derived keys to subscribe only to those
  • Subscribe all – omit keys to subscribe to everything (good for prototyping or small modules)
<script setup>
import { useDirective } from '@directive-run/vue';
import { counterModule } from './counterModule';

// Subscribe all: omit keys for everything
const { system, facts, derived, events, dispatch } = useDirective(counterModule);
</script>

<template>
  <div>
    <p>Count: {{ facts.count }}, Doubled: {{ derived.doubled }}</p>
    <button @click="events.increment()">+</button>
  </div>
</template>

With system config and selective subscriptions:

<script setup>
import { useDirective } from '@directive-run/vue';
import { counterModule } from './counterModule';

// Selective: subscribe to specific keys only
const { facts, derived, dispatch } = useDirective(counterModule, {
  facts: ['count'],
  derived: ['doubled'],
  plugins: [loggingPlugin()],
});
</script>

Core Composables

useSelector

The go-to composable for transforms and derived values from facts. Directive auto-tracks which fact keys your selector reads and subscribes only to those:

<script setup>
import { useSelector, shallowEqual } from '@directive-run/vue';
import { system } from './system';

// Transform a single fact value
const upperName = useSelector(system, (state) => state.user?.name?.toUpperCase() ?? 'GUEST');

// Extract a slice from state
const itemCount = useSelector(system, (state) => state.items?.length ?? 0);

// Combine values from multiple facts and derivations
const summary = useSelector(
  system,
  (state) => ({
    userName: state.user?.name,
    itemCount: state.items?.length ?? 0,
  }),
  (a, b) => a.userName === b.userName && a.itemCount === b.itemCount
);

// Custom equality to prevent unnecessary updates on array/object results
const ids = useSelector(
  system,
  (facts) => facts.users?.map(u => u.id) ?? [],
  shallowEqual,
);
</script>

<template>
  <p>{{ summary.userName }} has {{ summary.itemCount }} items</p>
</template>

useFact

Read a single fact or multiple facts:

<script setup>
import { useFact } from '@directive-run/vue';
import { system } from './system';

// Subscribe to a single fact – re-renders when "userId" changes
const userId = useFact(system, 'userId');

// Subscribe to multiple facts at once
const { name, email, avatar } = useFact(system, ['name', 'email', 'avatar']);
</script>

Need a transform?

Use useSelector to derive values from facts. It auto-tracks dependencies and supports custom equality.

useDerived

Read a single derivation or multiple derivations:

<script setup>
import { useDerived } from '@directive-run/vue';
import { system } from './system';

// Subscribe to a single derivation
const displayName = useDerived(system, 'displayName');

// Subscribe to multiple derivations at once
const { isLoggedIn, isAdmin } = useDerived(system, ['isLoggedIn', 'isAdmin']);
</script>

Need a transform?

Use useSelector to derive values from facts with auto-tracking and custom equality support.

useEvents

Get a typed reference to the system's event dispatchers:

<script setup>
import { useEvents } from '@directive-run/vue';
import { system } from './system';

// Get typed event dispatchers for the module
const events = useEvents(system);
</script>

<template>
  <button @click="events.increment()">+</button>
  <button @click="events.setCount({ count: 0 })">Reset</button>
</template>

useDispatch

Low-level event dispatch for untyped or system events:

<script setup>
import { useDispatch } from '@directive-run/vue';
import { system } from './system';

// Get the low-level dispatch function
const dispatch = useDispatch(system);
</script>

<template>
  <button @click="dispatch({ type: 'setUserId', userId: 42 })">
    Load User
  </button>
</template>

useWatch

Watch a fact or derivation for changes without causing re-renders – runs a callback as a side effect. useWatch auto-detects whether the key refers to a fact or a derivation, so there is no need to pass a discriminator:

<script setup>
import { useWatch } from '@directive-run/vue';
import { system } from './system';

// Watch a derivation for analytics tracking
useWatch(system, 'phase', (newPhase, oldPhase) => {
  analytics.track('phaseChange', { from: oldPhase, to: newPhase });
});

// Watch a fact – auto-detected, no "fact" discriminator needed
useWatch(system, 'userId', (newId, oldId) => {
  analytics.track('userId_changed', { from: oldId, to: newId });
});
</script>

Deprecated pattern

The four-argument form useWatch(system, "fact", "key", cb) still works but is deprecated. Use useWatch(system, "key", cb) instead – useWatch now auto-detects whether the key is a fact or derivation.


Inspection

useInspect

Get system inspection data (unmet requirements, inflight resolvers, constraint status) as a reactive ShallowRef<InspectState>:

<script setup>
import { useInspect } from '@directive-run/vue';
import { system } from './system';

// Get reactive system inspection data
const inspection = useInspect(system);
// InspectState: { isSettled, unmet, inflight, isWorking, hasUnmet, hasInflight }
</script>

<template>
  <Spinner v-if="inspection.isWorking" />
  <pre v-else>
    Settled: {{ inspection.isSettled }}
    Unmet: {{ inspection.unmet.length }}
    Inflight: {{ inspection.inflight.length }}
  </pre>
</template>

With throttling for high-frequency updates:

<script setup>
import { useInspect } from '@directive-run/vue';
import { system } from './system';

// Throttle updates to limit render frequency
const inspection = useInspect(system, { throttleMs: 200 });
</script>

useConstraintStatus

Read constraint status reactively:

<script setup>
import { useConstraintStatus } from '@directive-run/vue';
import { system } from './system';

// Get all constraints for the debug panel
const constraints = useConstraintStatus(system);
// Array<{ id: string; active: boolean; priority: number }>

// Check a specific constraint by ID
const auth = useConstraintStatus(system, 'requireAuth');
// { id: "requireAuth", active: true, priority: 50 } | null
</script>

useExplain

Get a reactive explanation of why a requirement exists:

<script setup>
import { useExplain } from '@directive-run/vue';
import { system } from './system';

// Get a detailed explanation of why a requirement was generated
const explanation = useExplain(system, 'FETCH_USER');
</script>

<template>
  <pre v-if="explanation">{{ explanation }}</pre>
  <p v-else>No active requirement</p>
</template>

Requirement Status Composables

These composables require a statusPlugin created via createRequirementStatusPlugin:

import { createSystem, createRequirementStatusPlugin } from '@directive-run/core';

// Create the status plugin for tracking requirement resolution
const statusPlugin = createRequirementStatusPlugin();

// Pass the plugin when creating the system
const system = createSystem({
  module: myModule,
  plugins: [statusPlugin.plugin],
});
system.start();

export { system, statusPlugin };

useRequirementStatus

Pass the statusPlugin as the first parameter:

<script setup>
import { useRequirementStatus } from '@directive-run/vue';
import { statusPlugin } from './system';

// Track a single requirement type
const status = useRequirementStatus(statusPlugin, 'FETCH_USER');
// status: { isLoading, hasError, pending, inflight, failed, lastError }

// Track multiple requirement types at once
const statuses = useRequirementStatus(statusPlugin, ['FETCH_USER', 'FETCH_SETTINGS']);
// statuses: Record<string, RequirementTypeStatus>
</script>

<template>
  <Spinner v-if="status.isLoading" />
  <Error v-else-if="status.hasError" :message="status.lastError?.message" />
  <UserContent v-else />
</template>

useOptimisticUpdate

Apply optimistic mutations with automatic rollback on resolver failure:

<script setup>
import { useOptimisticUpdate } from '@directive-run/vue';
import { system, statusPlugin } from './system';

// Set up optimistic mutations with automatic rollback
const { mutate, isPending, error, rollback } = useOptimisticUpdate(system, statusPlugin, 'SAVE_DATA');

function handleSave() {
  mutate(() => {
    // Optimistically update the UI before the server responds
    system.facts.savedAt = Date.now();
    system.facts.status = 'saved';
  });
  // If "SAVE_DATA" resolver fails, facts are rolled back automatically
}
</script>

<template>
  <button :disabled="isPending" @click="handleSave">
    {{ isPending ? 'Saving...' : 'Save' }}
  </button>
</template>

Typed Composables

Create fully typed composables for your module schema. Returned hooks take system as the first parameter:

import { createTypedHooks } from '@directive-run/vue';

// Create typed composables – full autocomplete for keys and events
const { useDerived, useFact, useDispatch, useEvents } =
  createTypedHooks<typeof myModule.schema>();
<script setup>
import { useFact, useDerived, useDispatch, useEvents } from './typed-hooks';
import { system } from './system';

// Fully typed – return types are inferred from the schema
const count = useFact(system, 'count');       // Type: Ref<number | undefined>
const doubled = useDerived(system, 'doubled'); // Type: Ref<number>
const dispatch = useDispatch(system);
const events = useEvents(system);

dispatch({ type: 'increment' });       // Typed!
events.increment();                    // Also typed!
</script>

Time-Travel Debugging

useTimeTravel returns a ShallowRef<TimeTravelState | null>null when disabled, otherwise the full reactive API. The ref auto-unwraps in templates, so you can access properties directly:

Undo / Redo Controls

<script setup>
import { useTimeTravel } from '@directive-run/vue';
import { system } from './system';

const timeTravel = useTimeTravel(system);
</script>

<template>
  <div v-if="timeTravel">
    <button @click="timeTravel.undo" :disabled="!timeTravel.canUndo">Undo</button>
    <button @click="timeTravel.redo" :disabled="!timeTravel.canRedo">Redo</button>
    <span>{{ timeTravel.currentIndex + 1 }} / {{ timeTravel.totalSnapshots }}</span>
  </div>
</template>

Snapshot Timeline

snapshots is lightweight metadata only (no facts data). Use getSnapshotFacts(id) to lazily load a snapshot's state on demand:

<template>
  <ul v-if="timeTravel">
    <li v-for="snap in timeTravel.snapshots" :key="snap.id">
      <button @click="timeTravel.goTo(snap.id)">
        {{ snap.trigger }} – {{ new Date(snap.timestamp).toLocaleTimeString() }}
      </button>
      <button @click="console.log(timeTravel.getSnapshotFacts(snap.id))">
        Inspect
      </button>
    </li>
  </ul>
</template>
<template>
  <div v-if="timeTravel">
    <button @click="timeTravel.goBack(5)">Back 5</button>
    <button @click="timeTravel.goForward(5)">Forward 5</button>
    <button @click="timeTravel.goTo(0)">Jump to Start</button>
    <button @click="timeTravel.replay()">Replay All</button>
  </div>
</template>

Session Persistence

<script setup>
import { useTimeTravel } from '@directive-run/vue';
import { system } from './system';

const timeTravel = useTimeTravel(system);

function saveSession() {
  if (timeTravel.value) {
    localStorage.setItem('debug', timeTravel.value.exportSession());
  }
}

function restoreSession() {
  const saved = localStorage.getItem('debug');
  if (saved && timeTravel.value) {
    timeTravel.value.importSession(saved);
  }
}
</script>

<template>
  <div v-if="timeTravel">
    <button @click="saveSession">Save Session</button>
    <button @click="restoreSession">Restore Session</button>
  </div>
</template>

Changesets

Group multiple fact mutations into a single undo/redo unit:

<script setup>
import { useTimeTravel } from '@directive-run/vue';
import { system } from './system';

const timeTravel = useTimeTravel(system);

function handleComplexAction() {
  timeTravel.value?.beginChangeset('Move piece A→B');
  // ... multiple fact mutations ...
  timeTravel.value?.endChangeset();
  // Now undo/redo treats all mutations as one step
}
</script>

<template>
  <button @click="handleComplexAction">Move Piece</button>
</template>

Recording Control

<template>
  <button v-if="timeTravel" @click="timeTravel.isPaused ? timeTravel.resume() : timeTravel.pause()">
    {{ timeTravel.isPaused ? 'Resume' : 'Pause' }} Recording
  </button>
</template>

See Time-Travel for the full TimeTravelState interface and keyboard shortcuts.


Patterns

Loading States

<script setup>
import { useFact, useDerived } from '@directive-run/vue';
import { system } from './system';

// Subscribe to the user fact
const user = useFact(system, 'user');

// Subscribe to the display name derivation
const displayName = useDerived(system, 'displayName');
</script>

<template>
  <Spinner v-if="!user" />
  <div v-else>
    <h1>{{ displayName }}</h1>
    <UserDetails :user="user" />
  </div>
</template>

Writing Facts

Write facts through the system directly:

<script setup>
import { useFact } from '@directive-run/vue';
import { system } from './system';

// Subscribe to the current userId
const userId = useFact(system, 'userId');
</script>

<template>
  <input
    type="number"
    :value="userId ?? 0"
    @input="system.facts.userId = parseInt(($event.target as HTMLInputElement).value)"
  />
</template>

Or dispatch events:

<script setup>
import { useDispatch } from '@directive-run/vue';
import { system } from './system';

const dispatch = useDispatch(system);
</script>

<template>
  <button @click="dispatch({ type: 'increment' })">+</button>
</template>

Testing

import { mount } from '@vue/test-utils';
import { createTestSystem } from '@directive-run/core/testing';
import { useFact, useDerived } from '@directive-run/vue';
import { userModule } from './modules/user';
import UserProfile from './UserProfile.vue';

test('displays user name', async () => {
  // Create a test system with mock data
  const system = createTestSystem({ module: userModule });
  system.facts.user = { id: 1, name: 'Test User' };

  // Components receive system explicitly – no plugin needed
  const wrapper = mount(UserProfile, {
    props: { system },
  });

  expect(wrapper.text()).toContain('Test User');
});

Utilities

shallowEqual

Re-exported from the core package for use with useSelector:

import { useSelector, shallowEqual } from '@directive-run/vue';
import { system } from './system';

// Use shallowEqual to prevent updates when x/y values haven't changed
const coords = useSelector(system, (state) => ({ x: state.position?.x, y: state.position?.y }), shallowEqual);

API Reference

ExportTypeDescription
useFactComposableRead single/multi facts
useDerivedComposableRead single/multi derivations
useSelectorComposableSelect from all facts with custom equality
useEventsComposableTyped event dispatchers
useDispatchComposableLow-level event dispatch
useWatchComposableSide-effect watcher for facts or derivations (auto-detects kind)
useInspectComposableSystem inspection (unmet, inflight, settled)
useConstraintStatusComposableReactive constraint inspection
useExplainComposableReactive requirement explanation
useRequirementStatusComposableSingle/multi requirement status (takes statusPlugin)
useOptimisticUpdateComposableOptimistic mutations with rollback
useDirectiveComposableScoped system with selected or all subscriptions
createTypedHooksFactoryCreate typed composables for a schema
useTimeTravelComposableReactive time-travel state (canUndo, canRedo, undo, redo)
shallowEqualUtilityShallow equality for selectors

Next Steps

Previous
React
Next
Svelte

We care about your data. We'll never share your email.

Powered by Directive. This signup uses a Directive module with facts, derivations, constraints, and resolvers – zero useState, zero useEffect. Read how it works

Directive - Constraint-Driven State Management for TypeScript