Mobx Query

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:

todo.entity.ts
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 accessor uses TC39 decorators (not legacy). See Installation for TypeScript config.
  • updateMutation.mutate() is an imperative call — it checks isDirty internally and skips the request if nothing has changed.
  • id defaults 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):

todos.store.ts
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:

mqclient.ts
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:

App.tsx
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:

TodoApp.tsx
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 new Todo to the collection before the server responds.
  • Clicking a todo calls toggleCompleted() — a MobX action that updates local state and triggers updateMutation.mutate() to sync with the server.
  • The UI re-renders instantly via observer() — before the server responds.
  • If the same Todo entity 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
  • QueriesQueryOne, QueryFragment, prefetching, and cache management
  • MutationsUpdateMutation, DeleteMutation, optimistic strategies, and rollback

On this page