Skip to main content

Integrations

5 min read

Integrations

Directive exposes standard reactive primitives that connect naturally to any state management library. Run side-by-side, or migrate fully with step-by-step guides.


Philosophy: Better Together

Directive doesn't replace your existing tools. It adds a constraint layer on top of them.

Your existing library handles what your state looks like and how it changes. Directive adds when – constraints that evaluate across state and automatically trigger the right behavior.

  • Redux dispatches actions → Directive decides when actions need dispatching
  • Zustand holds UI state → Directive evaluates rules across that state
  • XState transitions machines → Directive coordinates multiple machines
  • React Query fetches data → Directive decides when to fetch, prefetch, or invalidate

The result: your existing library keeps doing what it's good at, while Directive handles orchestration that would otherwise be scattered across useEffects and event handlers.


Interop Primitives

Directive ships six primitives that make external integration trivial:

PrimitiveSignatureUse case
system.subscribe(keys: string[], fn: () => void) => () => voidReact to fact/derivation changes, push into external stores
system.watch(key: string, fn: (value, prev) => void, opts?) => () => voidFine-grained sync with previous value comparison
system.batch(fn: () => void) => voidBulk-import external state without notification storms
system.dispatch(event: { type: string; ... }) => voidForward external actions as Directive events
system.getDistributableSnapshot(options?) => { data, createdAt, ... }Serialize full state for any consumer
Plugin onFactSet(key: string, value: unknown, prev: unknown) => voidIntercept every fact write for devtools/logging

General Pattern: External → Directive

Subscribe to the external store and batch-write into Directive facts. Always use batch() to coalesce multiple fact writes into a single notification cycle:

const unsubscribe = externalStore.subscribe((state) => {
  system.batch(() => {
    system.facts.count = state.count;
    system.facts.status = state.status;
  });
});

// Clean up when done
// unsubscribe();

Always batch multi-key writes

Without batch(), each fact assignment fires its own notification cycle. This can cause derivations and constraints to evaluate with partially-updated state.

Error Handling

External subscriptions can fire during teardown or with unexpected values. Wrap the sync body:

const unsubscribe = externalStore.subscribe((state) => {
  try {
    system.batch(() => {
      system.facts.count = state.count;
    });
  } catch (err) {
    console.error('Sync from external store failed:', err);
  }
});

General Pattern: Directive → External

Watch Directive facts and push changes to the external store. The watch callback receives both the new and previous value:

const unwatch = system.watch('count', (value, prev) => {
  externalStore.setState({ count: value });
});

// Clean up when done
// unwatch();

Use the equalityFn option to control when the callback fires:

const unwatch = system.watch('derivedResult', (value) => {
  externalStore.setState({ result: value });
}, {
  equalityFn: (a, b) => JSON.stringify(a) === JSON.stringify(b),
});

Choosing the Right Primitive

ScenarioPrimitiveWhy
Import multiple keys from external storesystem.batch() inside external subscribeCoalesces writes, single notification
React to a single Directive key changingsystem.watch(key, fn)Gives you new + previous value
React to any of several keys changingsystem.subscribe([keys], fn)Single listener for multiple keys
Mirror every fact change to devtoolsPlugin onFactSetFires for every write, zero overhead to register
Forward external actions into Directivesystem.dispatch(event)Events flow into event handlers and constraints
Export full state for serializationsystem.getDistributableSnapshot()Full state capture with metadata

Lifecycle and Cleanup

Every interop subscription must be cleaned up. In React, use useEffect:

useEffect(() => {
  // External → Directive
  const unsub = externalStore.subscribe((state) => {
    system.batch(() => {
      system.facts.value = state.value;
    });
  });

  // Directive → External
  const unwatch = system.watch('result', (value) => {
    externalStore.setState({ result: value });
  });

  return () => {
    unsub();
    unwatch();
  };
}, [system, externalStore]);

For framework-agnostic code, call the cleanup functions when your component or service tears down.


Avoiding Infinite Loops

When syncing bidirectionally, a change in Store A updates Store B, which triggers a change back in Store A. Prevent this with a guard flag:

let syncing = false;

// External → Directive
externalStore.subscribe((state) => {
  if (syncing) {
    return;
  }

  syncing = true;
  system.batch(() => {
    system.facts.count = state.count;
  });
  syncing = false;
});

// Directive → External
system.watch('count', (value) => {
  if (syncing) {
    return;
  }

  syncing = true;
  externalStore.setState({ count: value });
  syncing = false;
});

Any subscribe API works

Using a library not listed below? The general pattern above works with any store that exposes a subscribe API.


Library Guides

LibraryWhat it addsKey pattern
ReduxPredictable state + DevToolsstore.subscribe(() => { const s = store.getState(); ... }) – listener gets no args
ZustandMinimal UI statestore.subscribe((state, prev) => ...) – listener gets both current and previous state
XStateState machines + actorsactor.subscribe(fn) returns { unsubscribe }, not a bare function
React QueryServer cache + fetchingqueryCache.subscribe(event => ...) – event-driven with typed event objects

First-Party Adapters

These are built into Directive and don't use the subscribe patterns above:

AdapterWhat it does
Web WorkerRun the Directive engine off the main thread with a type-safe client

Migration Guides

Ready to go all-in? These guides walk through a full migration, concept by concept:

FromKey Mapping
ReduxSlices → Modules, actions → events, selectors → derivations, thunks → resolvers
ZustandStores → Modules, set → events, get → derivations, middleware → plugins
XStateMachines → Modules, states → facts, transitions → events, services → resolvers

All three guides follow the same pattern:

  1. Analyze – Map your existing concepts to Directive equivalents
  2. Create module – Define schema, init, events, derive, constraints, resolvers
  3. Coexist – Run both systems side-by-side using the interop patterns above
  4. Migrate UI – Replace store hooks with Directive hooks
  5. Remove old store – Once fully migrated, remove the old state library

Next Steps

  • Pick the library you're using and follow its guide above
  • Installation – Get Directive installed in your project
  • Core API – Full reference for subscribe, watch, batch, and dispatch
  • Plugins – Use plugin hooks for cross-cutting interop logic
Previous
Test Async Chains
Next
Redux

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