Skip to main content

Data Fetching

6 min read

Data Fetching

Declarative data fetching built on Directive's constraint engine.

@directive-run/query brings automatic cache invalidation, causal debugging, and time-travel to your data layer. No query keys. No manual invalidation. Change a fact, the query refetches.


Install

npm install @directive-run/query @directive-run/core

Choose Your Path

PathWhen to useSetup
createQuerySystemMost apps. Single module, bound handles, auto-start.1 function, 1 import
createQueryModuleMulti-module systems. Compose query modules with auth, UI, etc.createQueryModule + createSystem
createQuery + withQueriesFull control. Custom constraints, resolvers, cross-module deps.createQuery + withQueries + createModule + createSystem

Quick Start

import { createQuerySystem } from "@directive-run/query";

const app = createQuerySystem({
  facts: { userId: "" },
  queries: {
    user: {
      key: (f) => f.userId ? { userId: f.userId } : null,
      fetcher: async (p, signal) => {
        const res = await fetch(`/api/users/${p.userId}`, { signal });
        return res.json();
      },
    },
  },
  mutations: {
    updateUser: {
      mutator: async (vars, signal) => {
        const res = await fetch(`/api/users/${vars.id}`, {
          method: "PATCH",
          body: JSON.stringify(vars),
          signal,
        });
        return res.json();
      },
      invalidates: ["users"],
    },
  },
});

app.facts.userId = "42";                      // query fires automatically
const { data, isPending } = app.read("user"); // ResourceState
app.queries.user.refetch();                   // bound handle
app.mutations.updateUser.mutate({ id: "42", name: "New" });
app.explain("user");                          // "why did that fetch?"

React

const { data, isPending, error } = useDerived(system, "user");

Why Not TanStack Query?

TanStack Query is excellent. Directive Query adds things no competitor can:

  1. Causal cache invalidation – no query keys, no manual invalidation. Change a fact, the query refetches.
  2. explainQuery("user") – "Why did that fetch?" Full causal chain.
  3. Time-travel through API responses – cache is facts, facts are snapshotted.
  4. Constraint composition – queries depend on queries via auto-tracked facts.

ResourceState

Every query and subscription exposes a ResourceState<T>:

interface ResourceState<T> {
  data: T | null;
  error: Error | null;
  status: "pending" | "error" | "success";
  isPending: boolean;
  isFetching: boolean;
  isStale: boolean;
  isSuccess: boolean;
  isError: boolean;
  isPreviousData: boolean;
  dataUpdatedAt: number | null;
  failureCount: number;
  failureReason: Error | null;
}

Real-World Examples

React App with Queries + Mutations

import { useQuerySystem, useDerived } from "@directive-run/react";
import { createQuerySystem } from "@directive-run/query";

function App() {
  const app = useQuerySystem(() => createQuerySystem({
    facts: { userId: "", search: "" },
    queries: {
      user: {
        key: (f) => f.userId ? { userId: f.userId } : null,
        fetcher: async (p, signal) => {
          const res = await fetch(`/api/users/${p.userId}`, { signal });
          return res.json();
        },
        tags: ["users"],
        refetchAfter: 30_000,
      },
      searchResults: {
        key: (f) => f.search ? { q: f.search } : null,
        fetcher: async (p, signal) => {
          const res = await fetch(`/api/search?q=${p.q}`, { signal });
          return res.json();
        },
        expireAfter: 60_000,
      },
    },
    mutations: {
      updateProfile: {
        mutator: async (vars, signal) => {
          const res = await fetch(`/api/users/${vars.id}`, {
            method: "PATCH",
            body: JSON.stringify(vars),
            signal,
          });
          return res.json();
        },
        invalidates: ["users"],
      },
    },
    autoStart: false,
  }));

  const user = useDerived(app, "user");

  return (
    <div>
      <input
        placeholder="User ID"
        onChange={(e) => { app.facts.userId = e.target.value; }}
      />
      {user.isPending && <p>Loading...</p>}
      {user.isError && <p>Error: {user.error.message}</p>}
      {user.data && (
        <div>
          <h1>{user.data.name}</h1>
          <button onClick={() => {
            app.mutations.updateProfile.mutate({
              id: user.data.id,
              name: "Updated Name",
            });
          }}>
            Update
          </button>
        </div>
      )}
    </div>
  );
}

Multi-Module Dashboard

import { createSystem, createModule, t } from "@directive-run/core";
import {
  createQueryModule,
  createQuery,
  createMutation,
  createGraphQLClient,
} from "@directive-run/query";

// Data module – all API queries
const gql = createGraphQLClient({
  endpoint: "/api/graphql",
  headers: () => ({
    Authorization: `Bearer ${localStorage.getItem("token")}`,
  }),
});

const dataModule = createQueryModule("data", [
  gql.query({
    name: "dashboard",
    document: GetDashboardDocument,
    variables: () => ({ limit: 20 }),
    tags: ["dashboard"],
    refetchAfter: 60_000,
  }),
  gql.query({
    name: "notifications",
    document: GetNotificationsDocument,
    variables: () => ({ unreadOnly: true }),
    tags: ["notifications"],
  }),
  createMutation({
    name: "markRead",
    mutator: async (vars) => {
      await fetch(`/api/notifications/${vars.id}/read`, { method: "POST" });
    },
    invalidateTags: ["notifications"],
  }),
], {
  schema: { facts: { refreshToken: t.string() } },
  init: (f) => { f.refreshToken = ""; },
});

// Auth module – manages authentication state
const authModule = createModule("auth", {
  schema: {
    facts: { token: t.string(), user: t.object() },
    events: { login: { token: t.string() }, logout: {} },
    derivations: {},
    requirements: {},
  },
  init: (f) => {
    f.token = "";
    f.user = {};
  },
  events: {
    login: (f, { token }) => { f.token = token; },
    logout: (f) => {
      f.token = "";
      f.user = {};
    },
  },
});

// Compose into a single system
const system = createSystem({
  modules: { data: dataModule, auth: authModule },
});
system.start();

// Namespaced access
system.read("data.dashboard");          // Dashboard ResourceState
system.read("data.notifications");      // Notifications ResourceState
system.events.auth.login({ token });    // Auth events

AI RAG Pipeline with Query Data

import { createQuerySystem, createGraphQLClient } from "@directive-run/query";

const gql = createGraphQLClient({ endpoint: "/api/graphql" });

const app = createQuerySystem({
  facts: { question: "", context: "", answer: "" },

  queries: {
    // Step 1: Search for relevant documents
    relevantDocs: {
      key: (f) => f.question ? { q: f.question } : null,
      fetcher: async (p, signal) => {
        const res = await fetch(`/api/embeddings/search?q=${p.q}`, { signal });
        return res.json();
      },
    },
  },

  subscriptions: {
    // Step 2: Stream AI response using the documents as context
    aiResponse: {
      key: (f) => {
        const question = f.question as string;
        const docs = f._q_relevantDocs_state as { status: string; data: unknown };
        if (!question || docs?.status !== "success") {
          return null;
        }

        return { question, context: JSON.stringify(docs.data) };
      },
      subscribe: (params, { onData, onError, signal }) => {
        let response = "";
        fetch("/api/ai/chat", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            model: "claude-sonnet-4-5-20250514",
            messages: [
              {
                role: "system",
                content: `Answer using this context:\n${params.context}`,
              },
              { role: "user", content: params.question },
            ],
            stream: true,
          }),
          signal,
        }).then(async (res) => {
          const reader = res.body.getReader();
          const decoder = new TextDecoder();
          while (true) {
            const { done, value } = await reader.read();
            if (done) break;
            response += decoder.decode(value);
            onData(response);
          }
        }).catch((err) => {
          if (!signal.aborted) onError(err);
        });
      },
    },
  },
});

// Ask a question – docs fetch automatically, then AI streams the answer
app.facts.question = "How do constraints work in Directive?";

What's Next

Previous
Vanilla API

Stay in the loop. Sign up for our newsletter.

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 Runtime for TypeScript | AI Guardrails & State Management