Data Fetching
•4 min read
Mutations
Mutations handle write operations – POST, PUT, PATCH, DELETE. They can invalidate query caches by tag and support optimistic updates via lifecycle callbacks.
Basic Mutation
import { createMutation } from "@directive-run/query";
const updateUser = createMutation({
name: "updateUser",
mutator: async (vars: { id: string; name: string }, signal) => {
const res = await fetch(`/api/users/${vars.id}`, {
method: "PATCH",
body: JSON.stringify(vars),
signal,
});
return res.json();
},
});
Tag-Based Invalidation
When a mutation succeeds, it can invalidate queries by tag. All matching queries refetch automatically.
const updateUser = createMutation({
name: "updateUser",
mutator: async (vars, signal) => api.updateUser(vars),
invalidateTags: ["users"],
});
Tag matching supports wildcards:
| Mutation invalidates | Matches query tags |
|---|---|
"users" | "users", "users:42", "users:99" |
"users:42" | "users:42" only |
"users:*" | "users", "users:42", "users:99" |
In createQuerySystem, use the invalidates shorthand:
const app = createQuerySystem({
facts: {},
queries: {
user: { key: () => ({ id: "1" }), fetcher: api.getUser, tags: ["users"] },
},
mutations: {
updateUser: { mutator: api.updateUser, invalidates: ["users"] },
},
});
Optimistic Updates
Use onMutate to update the UI before the server responds. Return a context object for rollback.
const updateUser = createMutation({
name: "updateUser",
mutator: async (vars) => api.updateUser(vars),
invalidateTags: ["users"],
onMutate: (vars) => {
// Save previous data for rollback
return { previousName: "old name" };
},
onSuccess: (data, vars, context) => {
console.log("Updated:", data);
},
onError: (error, vars, context) => {
// Rollback using context
console.error("Failed, rolling back:", context.previousName);
},
onSettled: (data, error, vars, context) => {
// Always runs – success or error
},
});
MutationState
Mutations expose a MutationState derivation:
interface MutationState<TData, TError, TVariables> {
status: "idle" | "pending" | "success" | "error";
isPending: boolean;
isSuccess: boolean;
isError: boolean;
isIdle: boolean;
data: TData | null;
error: TError | null;
variables: TVariables | null;
}
Read it with system.read("updateUser") or useDerived(system, "updateUser").
Imperative Handles
// Advanced path
updateUser.mutate(system.facts, { id: "42", name: "New" });
const result = await updateUser.mutateAsync(system.facts, { id: "42", name: "New" });
updateUser.reset(system.facts);
// createQuerySystem (bound handles)
app.mutations.updateUser.mutate({ id: "42", name: "New" });
const result = await app.mutations.updateUser.mutateAsync({ id: "42", name: "New" });
app.mutations.updateUser.reset();
mutateAsync returns a Promise that resolves with the mutation result or rejects with the error. Supports concurrent calls – each gets its own promise.
Mutations in React – Optimistic Updates
import { useQuerySystem, useDerived } from "@directive-run/react";
import { createQuerySystem } from "@directive-run/query";
function TodoList() {
const app = useQuerySystem(() => createQuerySystem({
facts: { listId: "default" },
queries: {
todos: {
key: (f) => ({ listId: f.listId }),
fetcher: async (p, signal) => {
const res = await fetch(`/api/lists/${p.listId}/todos`, { signal });
return res.json();
},
tags: ["todos"],
},
},
mutations: {
addTodo: {
mutator: async (vars, signal) => {
const res = await fetch("/api/todos", {
method: "POST",
body: JSON.stringify(vars),
signal,
});
return res.json();
},
invalidates: ["todos"],
onMutate: (vars) => {
// Optimistic: add todo immediately
return { optimistic: true };
},
onError: (error, vars, context) => {
// Rollback on failure
console.error("Failed to add todo:", error);
},
},
toggleTodo: {
mutator: async (vars, signal) => {
const res = await fetch(`/api/todos/${vars.id}/toggle`, {
method: "PATCH",
signal,
});
return res.json();
},
invalidates: ["todos"],
},
},
autoStart: false,
}));
const todos = useDerived(app, "todos");
return (
<div>
<form onSubmit={(e) => {
e.preventDefault();
const input = e.currentTarget.elements.namedItem("title") as HTMLInputElement;
app.mutations.addTodo.mutate({ title: input.value });
input.value = "";
}}>
<input name="title" placeholder="New todo..." />
<button type="submit">Add</button>
</form>
{todos.data?.map((todo) => (
<label key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => app.mutations.toggleTodo.mutate({ id: todo.id })}
/>
{todo.title}
</label>
))}
</div>
);
}
Mutations with AI Agents
Use mutations to trigger AI processing and invalidate cached results:
const app = createQuerySystem({
facts: { documentId: "" },
queries: {
analysis: {
key: (f) => f.documentId ? { id: f.documentId } : null,
fetcher: async (p, signal) => {
const res = await fetch(`/api/documents/${p.id}/analysis`, { signal });
return res.json();
},
tags: ["analysis"],
},
},
mutations: {
reanalyze: {
mutator: async (vars, signal) => {
// Trigger AI re-analysis
const res = await fetch(`/api/ai/analyze`, {
method: "POST",
body: JSON.stringify({
documentId: vars.id,
model: "claude-sonnet-4-5-20250514",
}),
signal,
});
return res.json();
},
invalidates: ["analysis"], // re-fetch analysis after AI completes
},
},
});
// Fetch analysis
app.facts.documentId = "doc-123";
// Later: trigger re-analysis with AI
app.mutations.reanalyze.mutate({ id: "doc-123" });
// When the AI finishes, the "analysis" query automatically refetches

