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
factsand/orderivedkeys 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>
Navigation
<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
| Export | Type | Description |
|---|---|---|
useFact | Composable | Read single/multi facts |
useDerived | Composable | Read single/multi derivations |
useSelector | Composable | Select from all facts with custom equality |
useEvents | Composable | Typed event dispatchers |
useDispatch | Composable | Low-level event dispatch |
useWatch | Composable | Side-effect watcher for facts or derivations (auto-detects kind) |
useInspect | Composable | System inspection (unmet, inflight, settled) |
useConstraintStatus | Composable | Reactive constraint inspection |
useExplain | Composable | Reactive requirement explanation |
useRequirementStatus | Composable | Single/multi requirement status (takes statusPlugin) |
useOptimisticUpdate | Composable | Optimistic mutations with rollback |
useDirective | Composable | Scoped system with selected or all subscriptions |
createTypedHooks | Factory | Create typed composables for a schema |
useTimeTravel | Composable | Reactive time-travel state (canUndo, canRedo, undo, redo) |
shallowEqual | Utility | Shallow equality for selectors |
Next Steps
- Quick Start – Build your first module
- Facts – State management deep dive
- Testing – Testing Vue components

