Quickstart
Build your first mobx-query setup in 5 minutes.
This guide walks you through a complete working example — from defining an entity to rendering reactive data in a React component.
1. Define an Entity
An Entity is a MobX-observable class that represents a single record from your server or database. Extend the Entity base class and implement the required hydrate() method:
import { Entity, UpdateMutation } from "@mobx-query/core";
import { observable, action } from "mobx";
interface TodoData {
id: string;
title: string;
completed: boolean;
}
export class Todo extends Entity<TodoData> {
id: string = crypto.randomUUID();
@observable accessor title: string = "";
@observable accessor completed: boolean = false;
hydrate(data: TodoData) {
this.id = data.id;
this.title = data.title;
this.completed = data.completed;
}
readonly updateMutation = new UpdateMutation({
entity: Todo,
instance: this,
mutationFn: async () => {
await fetch(`/api/todos/${this.id}`, {
method: "PATCH",
body: JSON.stringify({ title: this.title, completed: this.completed }),
headers: { "Content-Type": "application/json" },
});
},
});
@action toggleCompleted() {
this.completed = !this.completed;
this.updateMutation.mutate();
}
}The hydrate() method is called whenever fresh data arrives from a query. It
maps raw data fields onto your observable properties. The Entity base class
handles identity, dirty tracking, and query hash management automatically.
A few things to note:
@observable accessoruses TC39 decorators (not legacy). See Installation for TypeScript config.updateMutation.mutate()is an imperative call — it checksisDirtyinternally and skips the request if nothing has changed.iddefaults to a UUID so optimistic creates work before the server responds.
2. Create a Store
A Store groups related queries and mutations for a specific domain. Define your queries using QueryMany (for lists) or QueryOne (for single records):
import { QueryMany, CreateMutation } from "@mobx-query/core";
import { Todo, type TodoData } 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() as Promise<TodoData[]>;
},
});
readonly createTodo = new CreateMutation({
entity: Todo,
mutationFn: async (input: { title: string }, entity) => {
await fetch("/api/todos", {
method: "POST",
body: JSON.stringify({ id: entity.id, ...input }),
headers: { "Content-Type": "application/json" },
});
},
});
}CreateMutation.mutationFn receives the input you pass when calling the
mutation and the entity instance that was optimistically created. Use
entity.id to send the client-generated ID to the server so both sides stay
in sync.
3. Initialize the Client
Create an MQClient instance — the root of your mobx-query setup. It registers your entity classes, creates the root store, and connects everything to TanStack Query:
import { QueryClient } from "@tanstack/react-query";
import { MQClient, createReactContext } from "@mobx-query/core";
import { Todo } from "./todo.entity";
import { TodosStore } from "./todos.store";
class RootStore {
todos = new TodosStore();
}
export function initMQClient(queryClient: QueryClient) {
return new MQClient<RootStore>({
context: { queryClient },
entities: [Todo],
rootStore: () => new RootStore(),
});
}
// Create typed React context helpers
export const { Provider: MQProvider, useContext: useMQ } =
createReactContext<MQClient<RootStore>>();Every entity class your app uses must be registered in the entities
array. This is how mobx-query creates the internal entity collections that
handle normalization and deduplication.
4. Set Up the Provider
Wrap your application with both TanStack Query's QueryClientProvider and the mobx-query MQProvider:
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { initMQClient, MQProvider } from "./mqclient";
const queryClient = new QueryClient();
const mqClient = initMQClient(queryClient);
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<MQProvider client={mqClient}>
<TodoApp />
</MQProvider>
</QueryClientProvider>
);
}5. Build a Reactive Component
Use your store's queries and mutations in React components. Wrap components with observer from mobx-react-lite to make them react to MobX observable changes:
import { observer } from "mobx-react-lite";
import { Suspense, useState } from "react";
import { useMQ } from "./mqclient";
import type { Todo } from "./todo.entity";
const TodoList = observer(() => {
const { rootStore } = useMQ();
const todos = rootStore.todos.todosQuery.useSuspenseQuery();
const createTodo = rootStore.todos.createTodo.useMutation();
const [title, setTitle] = useState("");
const handleCreate = () => {
if (!title.trim()) return;
createTodo({ title });
setTitle("");
};
return (
<div>
<div>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="What needs to be done?"
/>
<button onClick={handleCreate}>Add</button>
</div>
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
</div>
);
});
const TodoItem = observer(({ todo }: { todo: Todo }) => {
return (
<li
style={{ textDecoration: todo.completed ? "line-through" : "none" }}
onClick={() => todo.toggleCompleted()}
>
{todo.title}
</li>
);
});
export function TodoApp() {
return (
<Suspense fallback={<p>Loading todos...</p>}>
<TodoList />
</Suspense>
);
}That's it! Here's what's happening under the hood:
todosQuery.useSuspenseQuery()fetches data and returns hydrated MobX entity instances — not raw JSON.createTodo.useMutation()returns a function that optimistically adds a newTodoto the collection before the server responds.- Clicking a todo calls
toggleCompleted()— a MobX action that updates local state and triggersupdateMutation.mutate()to sync with the server. - The UI re-renders instantly via
observer()— before the server responds. - If the same
Todoentity appears in other queries, it's the same object instance — changes are visible everywhere.
What's Next?
Now that you have a working setup, explore the Guides to learn about:
- Defining Entities — entity lifecycle, hydrate patterns, and computed properties
- Queries —
QueryOne,QueryFragment, prefetching, and cache management - Mutations —
UpdateMutation,DeleteMutation, optimistic strategies, and rollback