Skip to main content

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 invalidatesMatches 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
Previous
Queries

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