TanStack Query + MobX — unified

Your entities deserve
first-class reactivity

mobx-query is the reactive bridge between TanStack Query and MobX. Get normalized entities, optimistic mutations, and dirty tracking out of the box — so you can focus on building features, not sync logic.

$npm install @mobx-query/core
TanStack Query v5MobX 6+React 18 / 19TypeScript-first

The Problem

Managing server state in MobX apps is painful

You either get raw JSON blobs with no reactivity, or hand-roll a fragile sync layer between TanStack Query and MobX.

Without mobx-query
  • Duplicate entity instances across queries
  • Manual cache ↔ MobX synchronization
  • Optimistic updates require bespoke rollback logic
  • No way to know which fields have changed locally
  • Edit forms need separate state management
  • Entity identity breaks on refetch
With mobx-query
  • One instance per entity — shared everywhere
  • Automatic hydration — query → entity mapping
  • Built-in optimistic mutations with rollback
  • Field-level dirty tracking out of the box
  • Entity is the form model — no duplication
  • Referential identity preserved across refetches

Core Features

Everything you need.
Nothing you don't.

Entity Normalization

Every query result passes through an EntityManager that deduplicates by ID. If Folder #42 appears in a sidebar query and a detail query, it's the same MobX-observable instance. Update it once — see the change everywhere.

  • Global identity map per entity type
  • Automatic merge on refetch
  • GC via query-hash reference counting
Sidebar Query
ID: 1ID: 2ID: 42
Detail Query
ID: 42
Folder #42single instance

Optimistic Mutations

Create, update, or delete entities and see it in the UI instantly — before the server responds. Snapshot-based rollback handles errors automatically. No manual cache manipulation needed.

  • CreateMutation — instant insertion with rollback
  • UpdateMutation — skips if entity isn't dirty
  • DeleteMutation — hides from all queries instantly
mutate()Entity added to UI instantly
state: pendingServer request in flight
✓ confirmedQueries invalidated
✗ rollbackState restored automatically

Dirty Tracking & Reset

Every entity automatically knows which fields have been locally modified. Your entity is your form model — change it directly, check isDirty, save when ready, or reset() to discard.

  • Deep-cloned snapshots per field
  • isDirty reactive property
  • reset() restores original server values
Folder entityisDirty: true
name"My Folder""Renamed"
isPinnedtrue
description"""Added desc"
save()reset()

Relation Mutations

Add / remove entities from many-to-many relationships with snapshot-based rollback.

Query Fragments

Nested queries owned by a parent entity — seeded from joins, fetched on demand.

Full TypeScript

Generic entities, typed mutations, and inferred context — zero @types needed.

Invalidation Strategies

Choose all-queries, related-queries, or none — per mutation.

Developer Experience

Three files. Full reactive stack.

Define an entity, write a query, render it — and get normalization, dirty tracking, and optimistic mutations for free.

todo.entity.ts1. Define
import { Entity, UpdateMutation } from "@mobx-query/core";
import { observable, action } from "mobx";

export class Todo extends Entity<string, TodoData> {
  @observable accessor title: string = "";
  @observable accessor completed: boolean = false;

  readonly updateMutation = new UpdateMutation({
    entity: Todo,
    instance: this,
    mutationFn: async () => {
      await fetch(`/api/todos/${this.id}`, {
        method: "PATCH",
        body: JSON.stringify({ completed: this.completed }),
      });
    },
  });

  @action toggleCompleted() {
    this.completed = !this.completed;
    this.updateMutation.mutate();
  }
}
todos.store.ts2. Query
import { QueryMany } from "@mobx-query/core";
import { Todo } from "./todo.entity";

export class TodosStore {
  readonly todosQuery = new QueryMany({
    entity: Todo,
    queryKey: () => ["todos"],
    queryFn: async () => {
      const res = await fetch("/api/todos");
      return res.json();
    },
  });
}
TodoList.tsx3. Render
import { observer } from "mobx-react-lite";

const TodoList = observer(() => {
  const { rootStore } = useMQ();
  const todos = rootStore.todos
    .todosQuery.useSuspenseQuery(undefined);

  return (
    <ul>
      {todos.map((todo) => (
        <li onClick={() => todo.toggleCompleted()}>
          {todo.title}
        </li>
      ))}
    </ul>
  );
});

Get Started in 5 Minutes

Your entities deserve
first-class reactivity

Stop writing glue code between TanStack Query and MobX. Let mobx-query handle normalization, caching, and mutations — so you can focus on building features.