Skip to main content

Resources

4 min read

FAQ

Common questions about Directive, answered.


Getting Started

What is Directive?

Directive is a constraint-driven state management library for TypeScript. Instead of manually dispatching actions or calling setters, you declare what must be true (constraints) and how to make it true (resolvers). The runtime automatically orchestrates state changes.

When should I use Directive?

Directive excels when your application has:

  • Complex interdependencies between state values
  • Async operations that depend on each other
  • Business rules that must always be satisfied
  • AI agents or workflows with multiple steps

For simple state (a counter, a toggle), Directive might be overkill. Consider starting with simpler solutions and migrating when complexity grows.

How is Directive different from Redux/Zustand?

AspectRedux/ZustandDirective
State updatesImperative (dispatch actions)Declarative (constraints)
DependenciesManual trackingAutomatic
Async handlingMiddleware (thunks, sagas)Built-in resolvers
Side effectsExternal (middleware)First-class (effects)
Time-travelPlugin requiredBuilt-in

How is Directive different from XState?

XState models explicit state machines with defined transitions. Directive models requirements that must be satisfied. XState is great for UI flows; Directive is great for data orchestration.


Constraints & Resolvers

Why didn't my constraint fire?

Common reasons:

  1. The when condition is false - Check that all conditions are met
  2. Another constraint has higher priority - Check priority values
  3. The requirement is already being resolved - Resolvers dedupe by default
  4. The system hasn't settled - Call await system.settle() to wait

Debug with the devtools plugin:

import { devtoolsPlugin } from '@directive-run/core/plugins';

// Attach devtools to see constraint evaluations and resolver activity
const system = createSystem({
  module: myModule,
  plugins: [devtoolsPlugin()],
});

Why is my resolver running multiple times?

  1. No deduplication key - Add a key function to your resolver
  2. The constraint fires repeatedly - Check if your when condition oscillates
  3. Facts are changing during resolution - Use context.facts carefully
resolvers: {
  fetchUser: {
    requirement: "FETCH_USER",

    // Dedupe by user ID so concurrent requests for the same user collapse
    key: (req) => `fetch-user-${req.payload.userId}`,

    resolve: async (req, context) => {
      // ...
    },
  },
},

What's the difference between effects and resolvers?

EffectsResolvers
Fire-and-forgetFulfill requirements
Run on fact changesRun when constraints activate
No retry/timeoutBuilt-in retry/timeout
Synchronous or asyncAlways async

Use effects for: logging, analytics, DOM updates, notifications. Use resolvers for: API calls, data loading, state transitions.


Performance

Are derivations expensive?

Derivations use dependency tracking and memoization. They only recompute when their dependencies change. For complex computations, they're often faster than manual memoization because tracking is automatic.

How many constraints are too many?

There's no hard limit. Constraint evaluation is O(n) where n is the number of constraints. In practice, hundreds of constraints work fine. If you have thousands, consider splitting into multiple modules.

Does Directive work with Server Components?

Yes! Directive is SSR-ready:

// Server: run the system and capture its state as a serializable snapshot
const system = createSystem({ module: myModule });
system.start();
const snapshot = system.getSnapshot();

// Client: restore from the server snapshot – no duplicate fetches
const clientSystem = createSystem({
  module: myModule,
  initialFacts: snapshot.facts,
});
clientSystem.start();

TypeScript

How do I get full type inference?

Use the t type builders in your schema:

import { t } from '@directive-run/core';

const myModule = createModule("app", {
  schema: {
    facts: {
      userId: t.number(),
      user: t.object<User>().nullable(),
      status: t.literal("idle", "loading", "error"),  // Union of string literals
    },
  },
  // Types flow automatically to constraints, resolvers, and hooks
});

Why are my types not inferring?

Common issues:

  1. Missing explicit type on object - Use t.object<MyType>()
  2. Circular references - Break cycles with explicit types
  3. Complex unions - Simplify or use t.custom<MyType>()

React Integration

Do I need to wrap my app in a Provider?

No. Directive uses a system-first pattern where hooks take the system as their first parameter. No provider or context is needed:

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

// No Provider wrapper needed – pass the system directly
function MyComponent() {
  const count = useFact(system, "count");

  return <p>{count}</p>;
}

Why is my component re-rendering too often?

  1. Reading too broadly - Select only the specific facts you need
  2. Missing selector - Use useSelector for fine-grained subscriptions
  3. Derivation recreating - Check derivation dependencies
// Bad: re-renders on ANY change to the user object
const user = useFact(system, "user");

// Good: re-renders only when the name changes
const userName = useSelector(system, (state) => state.user?.name);

Getting Help

Where can I ask questions?

How do I report a bug?

  1. Check if it's already reported in GitHub Issues
  2. Create a minimal reproduction
  3. Include: Directive version, TypeScript version, error message, steps to reproduce

How do I contribute?

See our Contributing Guide. We welcome:

  • Bug fixes
  • Documentation improvements
  • Feature proposals (open an issue first)
  • Example applications

Next Steps

Previous
Glossary

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