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/coreThe 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.
- 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
- 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
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
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
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.
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();
}
}
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();
},
});
}
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.