Skip to main content

Framework Adapters

13 min read

React Adapter

Directive's React hooks use a system-first pattern: pass the system as the first argument, and TypeScript infers everything else. No context, no provider, no factory.


Installation

The React adapter is included in the main package:

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

Setup

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

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 hooks in your components:

import { useFact, useDerived, useEvents } from '@directive-run/react';
import { system } from './system';

function UserProfile() {
  // Subscribe to the user fact
  const user = useFact(system, "user");

  // Subscribe to the display name derivation
  const displayName = useDerived(system, "displayName");

  // Get typed event dispatchers
  const events = useEvents(system);

  return (
    <div>
      <h1>{displayName}</h1>
      <button onClick={() => events.setUserId({ userId: 42 })}>Load User</button>
    </div>
  );
}

Every hook takes the system as its first parameter. TypeScript infers the fact keys, derivation keys, and event types from the system reference – no manual generics needed.


Creating Systems

Every hook below requires a system reference. There are three ways to create one:

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

For most React apps, prefer useDirectiveRef so each component owns its own system lifecycle. Use a global system when multiple components need to share the same state.

The two hooks below are progressive shortcuts – each adds more automatic behavior:

HookCreates systemSubscribes to stateYou choose what to subscribe to
useDirectiveRefYesNo – use useFact, useDerived, etc. separatelyFull control
useDirectiveYesYes – selected keys, or everything if none specifiedYou pick the keys (or omit for all)

useDirectiveRef

Create a system scoped to a component's lifecycle. The system starts in a useEffect and is destroyed on unmount:

import { useDirectiveRef, useFact, useEvents } from '@directive-run/react';

function Counter() {
  // Create a scoped system tied to this component's lifecycle
  const system = useDirectiveRef(counterModule);

  // Subscribe to the current count
  const count = useFact(system, "count");

  // Get typed event dispatchers
  const events = useEvents(system);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => events.increment()}>+</button>
    </div>
  );
}

With status plugin:

function App() {
  // Create a scoped system with the status plugin enabled
  const { system, statusPlugin } = useDirectiveRef(myModule, { status: true });

  // Track the loading state of a specific requirement
  const status = useRequirementStatus(statusPlugin, "FETCH_DATA");

  // Subscribe to a fact value
  const count = useFact(system, "count");
  // ...
}

With initial facts and plugins:

// Create a scoped system with plugins and initial state
const system = useDirectiveRef(myModule, {
  initialFacts: { count: 10 },
  plugins: [loggingPlugin()],
  debug: { timeTravel: true },
});

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 (component re-renders only when selected keys change)
  • Subscribe all – omit keys to subscribe to everything (good for prototyping or small modules)
import { useDirective } from '@directive-run/react';

// Selective: subscribe to specific keys only
function Counter() {
  const { dispatch, facts, derived } = useDirective(counterModule, {
    facts: ["count"],
    derived: ["doubled"],
  });

  return (
    <div>
      <p>Count: {facts.count}, Doubled: {derived.doubled}</p>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
    </div>
  );
}

// Subscribe all: omit keys to get everything
function CounterFull() {
  const { system, facts, derived, events, dispatch } = useDirective(counterModule);

  return (
    <div>
      <p>Count: {facts.count}, Doubled: {derived.doubled}</p>
      <button onClick={() => events.increment()}>+</button>
    </div>
  );
}

System config (plugins, initialFacts, debug, etc.) goes in the same options object:

const { facts, derived, events, dispatch } = useDirective(counterModule, {
  initialFacts: { count: 10 },
  plugins: [loggingPlugin()],
  debug: { timeTravel: true },
  status: true, // adds statusPlugin to return value
});

Core Hooks

useSelector

The go-to hook for transforms and derived values from facts. Directive auto-tracks which fact keys your selector reads and subscribes only to those – no manual dependency lists:

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

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

// Custom equality to prevent unnecessary re-renders on array/object results
const ids = useSelector(
  system,
  (state) => state.users?.map(u => u.id) ?? [],
  undefined,
  shallowEqual,
);

Default values

Pass a default value as the 3rd parameter. The default is returned before the system starts (when using useDirectiveRef, start() runs in a useEffect after first render) or when the selector returns undefined:

// Without default – requires ?? fallback at each call site
const email = useSelector(system, (state) => state.email) ?? "";

// With default – cleaner, prevents React's "uncontrolled to controlled" warning
const email = useSelector(system, (state) => state.email, "");
const status = useSelector(system, (state) => state.status, "idle");
const canSubmit = useSelector(system, (state) => state.canSubmit, false);

When a default value is provided, the system parameter may be null or undefined. The hook returns the default and recomputes automatically when the system becomes available:

// Nullable system – useful for conditional or lazy initialization
const status = useSelector(maybeSystem, (state) => state.status, "idle");

Auto-tracking

useSelector uses Directive's tracking system to detect accessed fact keys at subscription time. Inline selectors work without useCallback – the hook stores the selector in a ref internally (Zustand pattern).

useFact

Read a single fact or multiple facts:

// 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"]);

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:

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

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

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:

function Counter() {
  // Get typed event dispatchers for the system
  const events = useEvents(system);

  return (
    <div>
      <button onClick={() => events.increment()}>+</button>
      <button onClick={() => events.setCount({ count: 0 })}>Reset</button>
    </div>
  );
}

The returned reference is stable across renders (memoized on the system instance).

useDispatch

Low-level event dispatch for untyped or system events:

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

// Send an event object directly
dispatch({ type: "increment" });

useWatch

Watch a fact or derivation for changes without causing re-renders – auto-detects whether the key is a fact or derivation:

// Watch a fact (auto-detected)
useWatch(system, "userId", (newValue, prevValue) => {
  analytics.track("userId_changed", { from: prevValue, to: newValue });
});

// Watch a derivation (auto-detected)
useWatch(system, "pageViews", (newValue, prevValue) => {
  analytics.track("pageViews", { from: prevValue, to: newValue });
});

SSR & Hydration

DirectiveHydrator

Wrap your app (or a subtree) with a distributable snapshot from the server:

// Server: generate a distributable snapshot
const snapshot = system.getDistributableSnapshot({
  includeDerivations: ['effectivePlan', 'canUseFeature'],
  ttlSeconds: 3600,
});

// Client: hydrate from the server snapshot
import { DirectiveHydrator, useHydratedSystem } from '@directive-run/react';

function App({ serverSnapshot }) {
  return (
    <DirectiveHydrator snapshot={serverSnapshot}>
      <Dashboard />
    </DirectiveHydrator>
  );
}

useHydratedSystem

Create a scoped system pre-populated with data from the server snapshot:

function Dashboard() {
  // Create a system pre-hydrated from the server snapshot
  const system = useHydratedSystem(dashboardModule);

  // Derivation starts with the server-rendered value
  const plan = useDerived(system, "effectivePlan");

  return <p>Plan: {plan}</p>;
}

Inspection

useInspect

Get system inspection data (unmet requirements, inflight resolvers, constraint status):

function DebugPanel() {
  // Get reactive system inspection data
  const inspection = useInspect(system);

  return (
    <pre>
      Unmet: {inspection.unmet.length}
      Inflight: {inspection.inflight.length}
    </pre>
  );
}

With throttling for high-frequency updates:

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

useConstraintStatus

Read constraint status reactively:

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

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

useExplain

Get a reactive explanation of why a requirement exists:

function RequirementDebug({ requirementId }) {
  // Get a detailed explanation of why a requirement exists
  const explanation = useExplain(system, requirementId);

  if (!explanation) {
    return <p>No active requirement</p>;
  }

  return <pre>{explanation}</pre>;
}

Async Status

These hooks require a statusPlugin:

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

// 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();

useRequirementStatus

// 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>

useSuspenseRequirement

Integrates with React Suspense – throws a promise while the requirement is pending:

import { Suspense } from 'react';

function UserProfile() {
  // Suspends rendering until the requirement resolves
  useSuspenseRequirement(statusPlugin, "FETCH_USER");

  return <div>User loaded!</div>;
}

function Dashboard() {
  // Suspends until both requirements resolve
  useSuspenseRequirement(statusPlugin, ["FETCH_USER", "FETCH_SETTINGS"]);

  return <div>Everything loaded!</div>;
}

function App() {
  return (
    // Show a fallback while requirements are being resolved
    <Suspense fallback={<Spinner />}>
      <UserProfile />
    </Suspense>
  );
}

Optimistic Updates

useOptimisticUpdate

Apply optimistic mutations with automatic rollback on resolver failure:

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

  const handleSave = () => {
    mutate(() => {
      // Optimistic update – applied immediately via system.batch()
      system.facts.savedAt = Date.now();
      system.facts.status = "saved";
    });
    // If "SAVE_DATA" resolver fails, facts are rolled back automatically
  };

  return (
    <button onClick={handleSave} disabled={isPending}>
      {isPending ? "Saving..." : "Save"}
    </button>
  );
}

Manual rollback is also available via rollback().


DevTools

DirectiveDevTools

Dev-only floating panel (tree-shaken in production):

import { DirectiveDevTools } from '@directive-run/react';

function App() {
  return (
    <>
      <MyApp />
      {/* Floating debug panel – tree-shaken in production */}
      <DirectiveDevTools system={system} position="bottom-right" defaultOpen={false} />
    </>
  );
}

Features:

  • Facts and derivations tables with live values
  • Unmet requirements and inflight resolver counts
  • Keyboard shortcut: press Escape to close
  • Accessible: proper table headers, focus management

Writing Facts

Write facts through the system directly:

function UserIdInput() {
  // Subscribe to the current userId
  const userId = useFact(system, "userId");

  return (
    <input
      type="number"
      value={userId ?? 0}
      onChange={(e) => { system.facts.userId = parseInt(e.target.value); }}
    />
  );
}

Testing

import { render, screen } from '@testing-library/react';
import { createTestSystem } from '@directive-run/core/testing';
import { useFact } from '@directive-run/react';
import { userModule } from './modules/user';

function UserProfile({ system }) {
  // Subscribe to the user fact
  const user = useFact(system, "user");

  return <p>{user?.name}</p>;
}

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' };

  render(<UserProfile system={system} />);

  expect(screen.getByText('Test User')).toBeInTheDocument();
});

API Reference

ExportTypeDescription
useFactHookRead single/multi facts
useDerivedHookRead single/multi derivations
useSelectorHookAuto-tracking selector over facts and derivations
useEventsHookTyped event dispatchers
useDispatchHookLow-level event dispatch
useWatchHookSide-effect watcher for facts or derivations
useInspectHookSystem inspection (unmet, inflight, constraints)
useRequirementStatusHookSingle/multi requirement status
useSuspenseRequirementHookSuspense integration for requirements
useDirectiveRefHookScoped system tied to component lifecycle
useDirectiveHookScoped system with selected or all subscriptions
useExplainHookReactive requirement explanation
useConstraintStatusHookReactive constraint inspection
useOptimisticUpdateHookOptimistic mutations with rollback
DirectiveDevToolsComponentFloating debug panel
DirectiveHydratorComponentSSR snapshot hydration provider
useHydratedSystemHookCreate system from hydration context
useTimeTravelHookReactive time-travel state (canUndo, canRedo, undo, redo)
shallowEqualUtilityShallow equality for selectors

Time-Travel Debugging

useTimeTravel returns null when disabled, otherwise a reactive TimeTravelState with the full API. Destructure to pull out exactly what you need:

Undo / Redo Controls

import { useTimeTravel } from '@directive-run/react';

function UndoRedo() {
  const timeTravel = useTimeTravel(system);

  if (!timeTravel) {
    return null;
  }

  const { canUndo, canRedo, undo, redo, currentIndex, totalSnapshots } = timeTravel;

  return (
    <div>
      <button onClick={undo} disabled={!canUndo}>Undo</button>
      <button onClick={redo} disabled={!canRedo}>Redo</button>
      <span>{currentIndex + 1} / {totalSnapshots}</span>
    </div>
  );
}

Snapshot Timeline

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

function SnapshotTimeline() {
  const timeTravel = useTimeTravel(system);

  if (!timeTravel) {
    return null;
  }

  const { snapshots, goTo, getSnapshotFacts } = timeTravel;

  return (
    <ul>
      {snapshots.map((snap) => (
        <li key={snap.id}>
          <button onClick={() => goTo(snap.id)}>
            {snap.trigger}{new Date(snap.timestamp).toLocaleTimeString()}
          </button>
          <button onClick={() => console.log(getSnapshotFacts(snap.id))}>
            Inspect
          </button>
        </li>
      ))}
    </ul>
  );
}
function NavigationControls() {
  const timeTravel = useTimeTravel(system);

  if (!timeTravel) {
    return null;
  }

  const { goBack, goForward, goTo, replay } = timeTravel;

  return (
    <div>
      <button onClick={() => goBack(5)}>Back 5</button>
      <button onClick={() => goForward(5)}>Forward 5</button>
      <button onClick={() => goTo(0)}>Jump to Start</button>
      <button onClick={replay}>Replay All</button>
    </div>
  );
}

Session Persistence

function SessionControls() {
  const timeTravel = useTimeTravel(system);

  if (!timeTravel) {
    return null;
  }

  const { exportSession, importSession } = timeTravel;

  return (
    <div>
      <button onClick={() => localStorage.setItem('debug', exportSession())}>
        Save Session
      </button>
      <button onClick={() => {
        const saved = localStorage.getItem('debug');
        if (saved) importSession(saved);
      }}>
        Restore Session
      </button>
    </div>
  );
}

Changesets

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

function BatchedAction() {
  const timeTravel = useTimeTravel(system);

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

  return <button onClick={handleComplexAction}>Move Piece</button>;
}

Recording Control

function RecordingToggle() {
  const timeTravel = useTimeTravel(system);

  if (!timeTravel) {
    return null;
  }

  const { isPaused, pause, resume } = timeTravel;

  return (
    <button onClick={isPaused ? resume : pause}>
      {isPaused ? 'Resume' : 'Pause'} Recording
    </button>
  );
}

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


Next Steps

Previous
Overview
Next
Vue

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