Skip to main content

Framework Adapters

12 min read

Solid Adapter

Directive provides first-class SolidJS integration with hooks that bridge Directive state into Solid signals for fine-grained reactivity. All hooks take the system as an explicit first parameter – no provider or context needed.


Installation

The Solid adapter is included in the main package:

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

Setup

Create a system and pass it directly to hooks – no provider wrapper needed:

import { createSystem } from '@directive-run/core';
import { userModule } from './modules/user';

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

// Pass `system` directly to hooks in any component
function App() {
  return <YourApp />;
}

Creating Systems

Every hook below requires a system as its 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 creation and destroys on cleanup

For most Solid apps, use a global system and pass it to hooks. 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)
import { useDirective } from '@directive-run/solid';
import { counterModule } from './modules/counter';

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

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

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

  return <p>{facts().count}</p>;
}

The module parameter must be a stable reference (defined outside the component). Inline objects will create a new system on every reactive update.


Core Hooks

All hooks below take system as their first parameter.

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:

import { useSelector, shallowEqual } from '@directive-run/solid';

function Summary() {
  // 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 with custom equality
  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,
  );

  return <p>{summary().userName} has {summary().itemCount} items</p>;
}

useFact

Read a single fact or multiple facts. Returns a reactive Accessor:

// Subscribe to a single fact – signal updates when "userId" changes
const userId = useFact(system, "userId");
// userId() => number | undefined

// Subscribe to multiple facts at once
const data = useFact(system, ["name", "email"]);
// data() => { name: string; email: string }

Need a transform?

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

Usage in a component:

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

  // Subscribe to the user object
  const user = useFact(system, "user");

  return (
    <div>
      <p>ID: {userId()}</p>
      <p>User: {user()?.name}</p>
    </div>
  );
}

useDerived

Read a single derivation or multiple derivations. Returns a reactive Accessor:

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

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

Need a transform?

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

Usage in a component:

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

  return <h1>Hello, {displayName()}!</h1>;
}

useEvents

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

function Counter() {
  // Get typed event dispatchers for the module
  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 (memoized on the system instance).

useDispatch

Low-level event dispatch for untyped or system events:

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

  return (
    <button onClick={() => dispatch({ type: "increment" })}>
      +1
    </button>
  );
}

useWatch

Watch a fact or derivation for changes – runs a callback as a side effect without creating a signal for rendering. The key is auto-detected as either a fact or derivation, so no discriminator is needed:

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

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

Deprecated: "fact" discriminator

The old useWatch("fact", key, callback) three-argument pattern still works but is deprecated. Use useWatch(system, key, callback) instead – the runtime auto-detects whether the key is a fact or derivation.

// Deprecated – still works but not recommended
useWatch("fact", "userId", (newValue, prevValue) => { /* ... */ });

// Preferred – auto-detects fact vs derivation
useWatch(system, "userId", (newValue, prevValue) => { /* ... */ });

Inspection

useInspect

Get system inspection data as a signal. Accepts an optional { throttleMs } parameter for high-frequency updates. Returns Accessor<InspectState>:

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

  return (
    <pre>
      Settled: {inspection().isSettled ? "Yes" : "No"}
      Unmet: {inspection().unmet.length}
      Inflight: {inspection().inflight.length}
      Working: {inspection().isWorking ? "Yes" : "No"}
    </pre>
  );
}

With throttling:

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

InspectState fields:

FieldTypeDescription
isSettledbooleanNo pending work
unmetArrayUnmet requirements
inflightArrayIn-flight resolvers
isWorkingbooleanHas inflight resolvers
hasUnmetbooleanHas unmet requirements
hasInflightbooleanHas in-flight resolvers

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(props) {
  // Get a detailed explanation of why a requirement was generated
  const explanation = useExplain(system, props.requirementId);

  return (
    <Show when={explanation()} fallback={<p>No active requirement</p>}>
      <pre>{explanation()}</pre>
    </Show>
  );
}

Async Status

These hooks require a statusPlugin instance. Create one and pass it to the hooks directly:

import { createRequirementStatusPlugin } from '@directive-run/core';
import { useRequirementStatus } from '@directive-run/solid';

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

// Pass statusPlugin directly to hooks that need it
function UserLoader() {
  const status = useRequirementStatus(statusPlugin, "FETCH_USER");
  // ...
}

useRequirementStatus

Get full status for a single requirement type or multiple types. Takes statusPlugin as the first parameter:

import { Show } from 'solid-js';

// Track the loading state of a specific requirement type
function UserLoader() {
  const status = useRequirementStatus(statusPlugin, "FETCH_USER");

  return (
    <Show when={!status().isLoading} fallback={<Spinner />}>
      <Show when={!status().hasError} fallback={<Error message={status().lastError?.message} />}>
        <UserContent />
      </Show>
    </Show>
  );
}

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

  return (
    <Show when={!statuses()["FETCH_USER"].isLoading}>
      <Dashboard />
    </Show>
  );
}

useSuspenseRequirement

Integrates with Solid's Suspense – throws a promise while the requirement is pending. Takes statusPlugin as the first parameter:

import { Suspense } from 'solid-js';

function UserProfile() {
  // Suspends rendering until the requirement resolves
  useSuspenseRequirement(statusPlugin, "FETCH_USER");
  // Only renders after FETCH_USER resolves
  return <div>User loaded!</div>;
}

// Multiple requirements
function Dashboard() {
  useSuspenseRequirement(statusPlugin, ["FETCH_USER", "FETCH_SETTINGS"]);
  // Only renders after both resolve
  return <div>Everything loaded!</div>;
}

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

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,    // optional – enables auto-rollback on resolver failure
    "SAVE_DATA"      // requirement type to watch
  );

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

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

Manual rollback is also available via rollback().


Signal Factories

Create signals outside of components. Useful for stores or other reactive contexts. Returns a tuple of [Accessor<T>, cleanup]:

createDerivedSignal

import { createDerivedSignal } from '@directive-run/solid';

const system = createSystem({ module: myModule });
system.start();

// Create a derivation signal outside of components
const [isRed, cleanup] = createDerivedSignal<boolean>(system, "isRed");

// Use isRed() anywhere
console.log(isRed());

// Clean up when done
cleanup();

createFactSignal

import { createFactSignal } from '@directive-run/solid';

const system = createSystem({ module: myModule });
system.start();

// Create a fact signal outside of components
const [phase, cleanup] = createFactSignal<string>(system, "phase");

console.log(phase());

cleanup();

Typed Hooks

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

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

// Create typed hooks – full autocomplete for keys and events
const {
  useDerived, useFact, useDispatch, useEvents
} = createTypedHooks<typeof myModule.schema>();

function Profile() {
  // Fully typed – fact key autocompletes, return type inferred
  const count = useFact(system, "count");       // Type: Accessor<number>
  const doubled = useDerived(system, "doubled"); // Type: Accessor<number>
  const dispatch = useDispatch(system);
  const events = useEvents(system);

  dispatch({ type: "increment" });       // Typed!
  events.increment();                    // Typed!
}

Time Travel

useTimeTravel returns an Accessor<TimeTravelState | null>null when disabled, otherwise the full reactive API. Call timeTravel() to read, and destructure inside <Show> to access properties:

Undo / Redo Controls

import { useTimeTravel } from '@directive-run/solid';
import { Show } from 'solid-js';

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

  return (
    <Show when={timeTravel()}>
      {(state) => {
        const { canUndo, canRedo, undo, redo, currentIndex, totalSnapshots } = state();

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

Snapshot Timeline

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

import { Show, For } from 'solid-js';

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

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

  return (
    <Show when={timeTravel()}>
      {(state) => {
        const { goBack, goForward, goTo, replay } = state();

        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>
        );
      }}
    </Show>
  );
}

Session Persistence

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

  return (
    <Show when={timeTravel()}>
      {(state) => {
        const { exportSession, importSession } = state();

        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>
        );
      }}
    </Show>
  );
}

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

  return (
    <Show when={timeTravel()}>
      {(state) => {
        const { isPaused, pause, resume } = state();

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

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


Patterns

Loading States

import { Show } from 'solid-js';

function UserCard() {
  // Subscribe to loading, error, and user states
  const loading = useFact(system, "loading");
  const error = useFact(system, "error");
  const user = useFact(system, "user");

  return (
    <Show when={!loading()} fallback={<Spinner />}>
      <Show when={!error()} fallback={<Error message={error()} />}>
        <Show when={user()} fallback={<EmptyState />}>
          <UserDetails user={user()!} />
        </Show>
      </Show>
    </Show>
  );
}

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}
      onInput={(e) => { system.facts.userId = parseInt(e.currentTarget.value); }}
    />
  );
}

Or dispatch events:

function IncrementButton() {
  const dispatch = useDispatch(system);

  return <button onClick={() => dispatch({ type: "increment" })}>+</button>;
}

Testing

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

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 directly – no provider needed
  render(() => <UserProfile system={system} />);

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

API Reference

ExportTypeDescription
useFactHookRead single/multi facts
useDerivedHookRead single/multi derivations
useSelectorHookSelect across all facts
useEventsHookTyped event dispatchers
useDispatchHookLow-level event dispatch
useWatchHookSide-effect watcher for facts or derivations
useInspectHookSystem inspection (unmet, inflight, settled) with optional throttle
useConstraintStatusHookReactive constraint inspection
useExplainHookReactive requirement explanation
useRequirementStatusHookSingle/multi requirement status (takes statusPlugin)
useSuspenseRequirementHookSuspense integration for requirements (takes statusPlugin)
useOptimisticUpdateHookOptimistic mutations with rollback
useDirectiveHookScoped system with selected or all subscriptions
createTypedHooksFactoryCreate fully typed hooks for a schema
createDerivedSignalFactoryCreate a derivation signal outside components
createFactSignalFactoryCreate a fact signal outside components
useTimeTravelHookReactive time-travel state (canUndo, canRedo, undo, redo)
shallowEqualUtilityShallow equality for selectors

Next Steps

Previous
Svelte
Next
Lit

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