Skip to main content

API Reference

11 min read

Lit Controllers

Lit controllers API reference. All controllers follow the Reactive Controller pattern – pass the host element and system to the constructor.


Quick Reference

Controllers

ExportTypeDescription
DerivedControllerControllerSubscribe to one or more derivations
FactControllerControllerSubscribe to a single fact
WatchControllerControllerSide-effect watcher for derivations or facts
InspectControllerControllerSystem inspection with optional throttle
ExplainControllerControllerReactive requirement explanation
ConstraintStatusControllerControllerReactive constraint inspection
RequirementStatusControllerControllerRequirement status tracking
OptimisticUpdateControllerControllerOptimistic mutations with rollback
ModuleControllerControllerScoped system tied to element lifecycle
SystemControllerControllerCreate system scoped to element lifecycle
TimeTravelControllerControllerReactive time-travel state

Selector Controller

ExportTypeDescription
DirectiveSelectorControllerControllerSelect across all facts

Factory Functions (Quick Reference)

ExportTypeDescription
createDerivedFactoryShorthand for new DerivedController
createFactFactoryShorthand for new FactController
createWatchFactoryShorthand for new WatchController
createInspectFactoryShorthand for new InspectController
createExplainFactoryShorthand for new ExplainController
createConstraintStatusFactoryShorthand for new ConstraintStatusController
createRequirementStatusFactoryShorthand for new RequirementStatusController
createOptimisticUpdateFactoryShorthand for new OptimisticUpdateController
createModuleFactoryShorthand for new ModuleController
createDirectiveSelectorFactoryShorthand for new DirectiveSelectorController

Non-Reactive Utilities (Quick Reference)

ExportTypeDescription
useDispatchFunctionTyped dispatch function
useEventsFunctionTyped event dispatchers
useTimeTravelFunctionNon-reactive time-travel access
getDerivedFunctionNon-reactive derivation getter
getFactFunctionNon-reactive fact getter
createTypedHooksFactoryCreate typed controllers for a schema
shallowEqualUtilityShallow equality for selectors
directiveContextContextLit context key for sharing systems

DerivedController

Subscribe to one or more derivations. The host element re-renders when any subscribed derivation value changes.

import { DerivedController } from '@directive-run/lit';

// Single derivation
new DerivedController<T>(host: ReactiveControllerHost, system: System, key: string)

// Multiple derivations
new DerivedController<T>(host: ReactiveControllerHost, system: System, keys: string[])
ParameterTypeDescription
hostReactiveControllerHostThe Lit element (this)
systemSystemThe Directive system
keystring | string[]Derivation key(s) to subscribe to
PropertyTypeDescription
.valueTSingle key: the derivation value. Array of keys: Record<string, unknown> mapping keys to values.
// Single derivation
class MyElement extends LitElement {
  // Subscribe to the cart total – re-renders when it changes
  private total = new DerivedController<number>(this, system, "total");

  render() {
    return html`<span>Total: ${this.total.value}</span>`;
  }
}

// Multiple derivations
class DashboardElement extends LitElement {
  // Subscribe to multiple derivations at once
  private stats = new DerivedController<Record<string, unknown>>(
    this, system, ["total", "average", "count"]
  );

  render() {
    const { total, average, count } = this.stats.value;

    return html`<div>${total} / ${average} / ${count}</div>`;
  }
}

FactController

Subscribe to a single fact. The host element re-renders when the fact value changes.

import { FactController } from '@directive-run/lit';

new FactController<T>(host: ReactiveControllerHost, system: System, key: string)
ParameterTypeDescription
hostReactiveControllerHostThe Lit element (this)
systemSystemThe Directive system
keystringFact key to subscribe to
PropertyTypeDescription
.valueTCurrent fact value
class StatusElement extends LitElement {
  // Subscribe to the current phase – re-renders when it changes
  private phase = new FactController(this, system, "phase");

  render() {
    return html`<span>Phase: ${this.phase.value}</span>`;
  }
}

DirectiveSelectorController

Select across all facts with an auto-tracking selector. The host element re-renders only when the selected value changes.

import { DirectiveSelectorController } from '@directive-run/lit';

new DirectiveSelectorController<R>(
  host: ReactiveControllerHost,
  system: System,
  selector: (state: FactsProxy) => R,
  equalityFn?: (a: R, b: R) => boolean,
)
ParameterTypeDescription
hostReactiveControllerHostThe Lit element (this)
systemSystemThe Directive system
selector(state: FactsProxy) => RSelector over facts and derivations
equalityFn(a: R, b: R) => booleanOptional custom equality check (defaults to ===)
PropertyTypeDescription
.valueRCurrent selected value
class SummaryElement extends LitElement {
  // Select across all facts to build a summary string
  private summary = new DirectiveSelectorController(
    this, system, (facts) => `${facts.user?.name}: ${facts.count}`
  );

  render() {
    return html`<p>${this.summary.value}</p>`;
  }
}

WatchController

Side-effect watcher for facts or derivations. The key is auto-detected, so no discriminator is needed. Fires a callback when the watched value changes. Does not expose a .value property.

import { WatchController } from '@directive-run/lit';

// Unified API – auto-detects fact vs derivation
new WatchController(
  host: ReactiveControllerHost,
  system: System,
  key: string,
  callback: (value: unknown, prev: unknown) => void,
)

// Deprecated: "fact" discriminator overload (still works)
new WatchController(
  host: ReactiveControllerHost,
  system: System,
  { kind: "fact", factKey: string },
  callback: (value: unknown, prev: unknown) => void,
)
ParameterTypeDescription
hostReactiveControllerHostThe Lit element (this)
systemSystemThe Directive system
keystringKey to watch (auto-detected as fact or derivation)
callback(value, prev) => voidCalled when the value changes
class LoggerElement extends LitElement {
  // Watch the phase derivation – auto-detected
  private _watcher = new WatchController(
    this, system, "phase", (value, prev) => {
      console.log(`Phase changed: ${prev} -> ${value}`);
    }
  );

  // Watch the count fact – auto-detected, no discriminator needed
  private _factWatcher = new WatchController(
    this, system, "count", (value, prev) => {
      console.log(`Count changed: ${prev} -> ${value}`);
    }
  );
}

Deprecated

The { kind: "fact", factKey: "key" } options object is deprecated. Pass the key as a plain string instead – the runtime auto-detects whether it is a fact or derivation.


InspectController

System inspection controller. Provides reactive access to unmet requirements, inflight resolvers, constraint statuses, and settlement state. Supports optional throttling.

import { InspectController } from '@directive-run/lit';

new InspectController(host: ReactiveControllerHost, system: System, opts?: { throttleMs?: number })
ParameterTypeDescription
hostReactiveControllerHostThe Lit element (this)
systemSystemThe Directive system
opts.throttleMsnumberOptional throttle interval in milliseconds
PropertyTypeDescription
.valueInspectStateCurrent inspection state
.value.unmetRequirement[]Currently unmet requirements
.value.inflightRequirement[]Requirements being resolved
.value.constraintsConstraintStatus[]Constraint statuses
.value.isSettledbooleanWhether the system is settled
class DebugElement extends LitElement {
  // Get reactive system inspection data with throttled updates
  private inspect = new InspectController(this, system, { throttleMs: 200 });

  render() {
    const { unmet, isSettled } = this.inspect.value;

    return html`
      <div>Settled: ${isSettled}</div>
      <div>Unmet: ${unmet.length}</div>
    `;
  }
}

ExplainController

Reactive requirement explanation. Provides a detailed breakdown of why a requirement exists and what constraint produced it.

import { ExplainController } from '@directive-run/lit';

new ExplainController(host: ReactiveControllerHost, system: System, requirementType: string)
ParameterTypeDescription
hostReactiveControllerHostThe Lit element (this)
systemSystemThe Directive system
requirementTypestringThe requirement type to explain
PropertyTypeDescription
.valueExplanation | nullCurrent explanation or null
class ExplainElement extends LitElement {
  // Get a detailed explanation of why the TRANSITION requirement exists
  private explanation = new ExplainController(this, system, "TRANSITION");

  render() {
    const exp = this.explanation.value;
    if (!exp) {
      return html`<p>No active requirement</p>`;
    }

    return html`<pre>${JSON.stringify(exp, null, 2)}</pre>`;
  }
}

ConstraintStatusController

Reactive constraint inspection. Subscribe to the status of a single constraint or all constraints.

import { ConstraintStatusController } from '@directive-run/lit';

new ConstraintStatusController(host: ReactiveControllerHost, system: System, constraintId?: string)
ParameterTypeDescription
hostReactiveControllerHostThe Lit element (this)
systemSystemThe Directive system
constraintIdstringOptional constraint ID. Omit to get all constraints.
PropertyTypeDescription
.valueConstraintStatus | ConstraintStatus[]Single status or array of all statuses
class ConstraintElement extends LitElement {
  // Check if the transition constraint is currently active
  private status = new ConstraintStatusController(this, system, "transition");

  render() {
    return html`<span>Active: ${this.status.value?.active}</span>`;
  }
}

RequirementStatusController

Track the status of a specific requirement type, including inflight count and last error.

import { RequirementStatusController } from '@directive-run/lit';

new RequirementStatusController(
  host: ReactiveControllerHost,
  statusPlugin: StatusPlugin,
  type: string,
)
ParameterTypeDescription
hostReactiveControllerHostThe Lit element (this)
statusPluginStatusPluginThe status plugin instance
typestringRequirement type to track
PropertyTypeDescription
.valueRequirementStatusCurrent status
.value.inflightnumberNumber of inflight resolutions
.value.lastErrorError | nullMost recent error
class LoadingElement extends LitElement {
  // Track the loading state of the FETCH_USER requirement
  private status = new RequirementStatusController(this, statusPlugin, "FETCH_USER");

  render() {
    if (this.status.value.inflight > 0) {
      return html`<spinner-el></spinner-el>`;
    }

    if (this.status.value.lastError) {
      return html`<p>Error!</p>`;
    }

    return html`<p>Ready</p>`;
  }
}

OptimisticUpdateController

Optimistic mutations with automatic rollback on failure.

import { OptimisticUpdateController } from '@directive-run/lit';

new OptimisticUpdateController(
  host: ReactiveControllerHost,
  system: System,
  statusPlugin: StatusPlugin,
  requirementType: string,
)
ParameterTypeDescription
hostReactiveControllerHostThe Lit element (this)
systemSystemThe Directive system
statusPluginStatusPluginThe status plugin instance
requirementTypestringRequirement type for optimistic updates
PropertyTypeDescription
.valueOptimisticStateCurrent optimistic state
.mutate()MethodApply an optimistic mutation
class LikeButton extends LitElement {
  // Set up optimistic mutations with automatic rollback
  private optimistic = new OptimisticUpdateController(
    this, system, statusPlugin, "TOGGLE_LIKE"
  );

  private handleClick() {
    // Optimistically update the UI before the server responds
    this.optimistic.mutate({ liked: true, count: currentCount + 1 });
  }
}

ModuleController

Creates a scoped system tied to the element lifecycle. The system starts on connectedCallback and stops on disconnectedCallback.

import { ModuleController } from '@directive-run/lit';

new ModuleController(host: ReactiveControllerHost, module: Module, opts?: ModuleOpts)
ParameterTypeDescription
hostReactiveControllerHostThe Lit element (this)
moduleModuleThe Directive module definition
opts.statusbooleanEnable the status plugin
opts.debugDebugOptsDebug options (time-travel, etc.)
opts.pluginsPlugin[]Additional plugins
PropertyTypeDescription
.systemSystemThe scoped system instance
class CounterElement extends LitElement {
  // Create a scoped system tied to this element's lifecycle
  private mod = new ModuleController(this, counterModule, { status: true });

  render() {
    const count = this.mod.system.read("count");

    return html`<button @click=${() => this.mod.system.dispatch({ type: "INCREMENT" })}>
      ${count}
    </button>`;
  }
}

SystemController

Create a full system scoped to the element lifecycle. Accepts either a module directly or a configuration object.

import { SystemController } from '@directive-run/lit';

// Simple
new SystemController(host: ReactiveControllerHost, module: Module)

// With options
new SystemController(host: ReactiveControllerHost, config: {
  module: Module,
  plugins?: Plugin[],
  debug?: DebugOpts,
})
ParameterTypeDescription
hostReactiveControllerHostThe Lit element (this)
moduleModuleThe Directive module definition
config.pluginsPlugin[]Optional plugins
config.debugDebugOptsOptional debug options
PropertyTypeDescription
.systemSystemThe scoped system instance
class AppElement extends LitElement {
  // Create a full system scoped to this element's lifecycle
  private ctrl = new SystemController(this, {
    module: appModule,
    plugins: [loggingPlugin()],
    debug: { timeTravel: true },
  });

  render() {
    // Pass the system down to child elements
    return html`<child-el .system=${this.ctrl.system}></child-el>`;
  }
}

TimeTravelController

Reactive time-travel state. Provides undo/redo capabilities and snapshot navigation.

import { TimeTravelController } from '@directive-run/lit';

new TimeTravelController(host: ReactiveControllerHost, system: System)
ParameterTypeDescription
hostReactiveControllerHostThe Lit element (this)
systemSystemThe Directive system (must have time-travel enabled)
PropertyTypeDescription
.valueTimeTravelState | nullCurrent time-travel state or null if not enabled
class TimeTravelElement extends LitElement {
  // Get reactive time-travel controls (null when not enabled)
  private tt = new TimeTravelController(this, system);

  render() {
    const state = this.tt.value;
    if (!state) {
      return html`<p>Time-travel not enabled</p>`;
    }

    return html`
      <button ?disabled=${!state.canUndo} @click=${state.undo}>Undo</button>
      <button ?disabled=${!state.canRedo} @click=${state.redo}>Redo</button>
    `;
  }
}

Factory Functions

Every controller has a corresponding factory function that serves as a shorthand. The factory functions accept the same arguments as their controller constructors.

import {
  createDerived,
  createFact,
  createWatch,
  createInspect,
  createExplain,
  createConstraintStatus,
  createRequirementStatus,
  createOptimisticUpdate,
  createModule,
  createDirectiveSelector,
} from '@directive-run/lit';

class MyElement extends LitElement {
  // These are equivalent:
  private total = new DerivedController(this, system, "total");
  private total = createDerived(this, system, "total");

  // Subscribe to a single fact
  private phase = createFact(this, system, "phase");

  // Subscribe to multiple derivations at once
  private stats = createDerived(this, system, ["total", "average"]);

  // Get system inspection data with throttled updates
  private inspect = createInspect(this, system, { throttleMs: 200 });

  // Get a requirement explanation
  private explanation = createExplain(this, system, "TRANSITION");

  // Check a specific constraint
  private constraint = createConstraintStatus(this, system, "transition");

  // Track requirement loading state
  private reqStatus = createRequirementStatus(this, statusPlugin, "FETCH_USER");

  // Set up optimistic mutations
  private optimistic = createOptimisticUpdate(this, system, statusPlugin, "TOGGLE_LIKE");

  // Create a scoped system tied to this element
  private mod = createModule(this, counterModule, { status: true });

  // Selector factory – derive values from facts
  private summary = createDirectiveSelector(this, system, (facts) => `${facts.count}`);
}

Non-Reactive Utilities

These functions return values or proxies directly without subscribing to changes. They do not trigger host re-renders.

import {
  useDispatch,
  useEvents,
  useTimeTravel,
  getDerived,
  getFact,
  createTypedHooks,
  shallowEqual,
  directiveContext,
} from '@directive-run/lit';

useDispatch

Returns a typed dispatch function for sending events to the system.

useDispatch(system: System): (event: SystemEvent) => void
// Get a typed dispatch function for sending events
const dispatch = useDispatch(system);

// Dispatch an event directly
dispatch({ type: "INCREMENT", payload: 1 });

useEvents

Returns typed event dispatchers. Each event type becomes a callable function.

useEvents(system: System): TypedEventDispatchers
// Get typed event dispatchers – each event type becomes a callable function
const events = useEvents(system);

// Dispatch events with full type safety
events.increment(1);
events.reset();

useTimeTravel

Returns the current time-travel state without subscribing to changes. For reactive subscriptions, use TimeTravelController instead.

useTimeTravel(system: System): TimeTravelState | null
// Get the current time-travel state (non-reactive)
const tt = useTimeTravel(system);

// Undo the last action if possible
if (tt?.canUndo) tt.undo();

getDerived

Returns a non-reactive getter function for a derivation value.

getDerived<T>(system: System, key: string): () => T
// Create a non-reactive getter for a derivation
const getTotal = getDerived(system, "total");

// Read the current value on demand
console.log(getTotal());

getFact

Returns a non-reactive getter function for a fact value.

getFact<T>(system: System, key: string): () => T
// Create a non-reactive getter for a fact
const getCount = getFact(system, "count");

// Read the current value on demand
console.log(getCount());

createTypedHooks

Creates a set of typed controller factories and utilities pre-bound to a specific schema. This eliminates the need for manual type annotations on every controller.

createTypedHooks<M extends ModuleSchema>(): {
  createDerived: <K>(host, system, derivationId: K) => DerivedController<InferDerivations<M>[K]>;
  createFact: <K>(host, system, factKey: K) => FactController<InferFacts<M>[K]>;
  useDispatch: (system) => (event: InferEvents<M>) => void;
  useEvents: (system) => System<M>["events"];
}
// Create typed controller factories for your schema
const hooks = createTypedHooks<typeof appModule>();

class MyElement extends LitElement {
  // Fully typed – fact key autocompletes, return type inferred
  private count = hooks.createFact(this, "count");

  // Derivation types are also fully inferred
  private total = hooks.createDerived(this, "total");
}

shallowEqual

Shallow equality comparison for use as an equalityFn in selector controllers.

shallowEqual(a: unknown, b: unknown): boolean
// Use shallowEqual to prevent re-renders when name/age haven't changed
const selected = new DirectiveSelectorController(
  this, system, (facts) => ({ name: facts.user?.name, age: facts.user?.age }), shallowEqual
);

directiveContext

A Lit context key for sharing systems through the component tree via @lit/context.

import { directiveContext } from '@directive-run/lit';
import { provide, consume } from '@lit/context';

// Provider – share the system with descendant elements
@provide({ context: directiveContext })
system = mySystem;

// Consumer – receive the system from an ancestor
@consume({ context: directiveContext })
system!: System;

Next Steps

Previous
Solid Hooks

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