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
| Path | When to use | Setup |
|---|---|---|
createQuerySystem | Most apps. Single module, bound handles, auto-start. | 1 function, 1 import |
createQueryModule | Multi-module systems. Compose query modules with auth, UI, etc. | createQueryModule + createSystem |
createQuery + withQueries | Full 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:
- Causal cache invalidation – no query keys, no manual invalidation. Change a fact, the query refetches.
explainQuery("user")– "Why did that fetch?" Full causal chain.- Time-travel through API responses – cache is facts, facts are snapshotted.
- 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
- Queries – pull-based data fetching
- Mutations – write operations with cache invalidation
- Subscriptions – WebSocket, SSE, AI streaming
- Infinite Queries – pagination and infinite scroll
- Convenience API –
createQuerySystemandcreateQueryModule - GraphQL – typed GraphQL queries with codegen support
- Explain & Debug – causal chain debugging

