Open-source project
xaviergonz/mobx-keystone avatar
xaviergonz/mobx-keystone

mobx-keystone adds snapshots and patches to a MobX data tree

A MobX powered state management solution based on data trees with first class support for Typescript, support for snapshots, patches and much more

629 stars28 forksTypeScriptMIT

At a glance

What is it?
mobx-keystone is a TypeScript-first state management library on top of MobX, where a tree of mutable but protected models gives you MobX ergonomics plus immutable snapshots, JSON patches, undo and redo, action middlewares and CRDT bindings for collaborative editing. It is MIT licensed and is a superset in spirit of mobx-state-tree.
Who is it for?
mobx-keystone suits a TypeScript team that wants MobX style mutable model code and also needs undo, persistence, sync or replay from the same state tree, since the snapshot and patch machinery is the reason to use it rather than plain MobX. It is the wrong tool if you want a Redux reducer and store pair, because the model-driven approach replaces that shape rather than wrapping it, and it is the wrong tool for server rendering that assumes a single serialisable store.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The proposition: mutable code, immutable record

mobx-keystone helps you build complex client-side apps with a single source of truth, mutable model code, and immutable traceability built in. The split is the point: you write straightforward actions and computed values, while the library gives you snapshots, patches, undo/redo, and runtime protection on top.

The description of it as a TypeScript-first model layer on top of MobX that scales better as your domain grows tells you where it sits in the ecosystem. It is a deliberate middle position between plain MobX, which gives you reactivity and nothing else, and mobx-state-tree, which gives you a data tree with snapshots and patches but a reputation for friction.

The How it works section is the most precise part of the README. At the center of mobx-keystone is a living tree of mutable but strictly protected models, arrays, and plain objects. You update state through model actions, and immutable structurally shared snapshots are derived automatically. The result is mutability where it helps developer experience, plus immutable traceability where it helps reliability.

Two properties of that design are worth understanding before anything else. Trees can only be modified by actions that belong to the same subtree, which is a stronger guarantee than most libraries offer and is what makes the snapshots trustworthy. And actions are replayable and can be distributed, with fine-grained changes observable as JSON patches.

Because mobx-keystone uses MobX behind the scenes, it integrates naturally with mobx and mobx-react-lite, and the snapshot and middleware system also makes it possible to replace a Redux reducer and store pair with model-driven state and connect Redux devtools.

The library consists of composable models that capture domain state and behaviour together. Model instances are created from props, protect their own updates, and reconcile efficiently when applying snapshots.

A todo store in thirty lines

The quick glance in the README is a complete, working example, and it is the fastest way to judge whether the API shape suits you.

ts
import { computed } from "mobx"
import { Model, model, modelAction, prop, registerRootStore } from "mobx-keystone"

@model("todo/Todo")
class Todo extends Model({
  text: prop<string>(""),
  done: prop(false),
}) {
  @modelAction
  toggle() {
    this.done = !this.done
  }
}

The model decorator names the model with a type identifier, and the props are declared through the prop function, which is where the type parameter comes from. The method is marked with modelAction, and inside it the field is assigned directly, which is the mutable model code the README talks about.

The store is the same pattern with an array of children and a computed value:

ts
@model("todo/Store")
class TodoStore extends Model({
  todos: prop<Todo[]>(() => []),
}) {
  @computed
  get pendingCount() {
    return this.todos.filter((t) => !t.done).length
  }

  @modelAction
  addTodo(text: string) {
    this.todos.push(new Todo({ text }))
  }
}

And the root is registered so that snapshots and patches can be taken from a known starting point:

ts
const store = new TodoStore({})
registerRootStore(store)

The array prop uses a thunk, prop<Todo[]>(() => []), which is how a default that must be constructed lazily is expressed, since a fresh array per instance is not the same thing as one shared array.

The last two lines are the ones to notice. registerRootStore is not part of most MobX tutorials, and its presence tells you that this library is designed around the idea that your application state has an identifiable root from which snapshots, patches and undo history are derived.

Installing it, and the runtime requirements

The installation is a peer dependency, and the README is explicit that both packages are needed.

code
npm install mobx mobx-keystone

The same for yarn:

code
yarn add mobx mobx-keystone

And for pnpm:

code
pnpm add mobx mobx-keystone

mobx is a peer dependency and must be installed alongside mobx-keystone, which is a change from the older versions of this library where MobX was a direct dependency. For a team that already has MobX in the tree, that is a non-event; for a team starting fresh, it means the versions have to be chosen together.

The requirements are a short list of environment capabilities: MobX 7, 6, 5, or 4 with its gotchas, then Proxies, Symbols, and WeakMap/WeakSet. In practice this covers current browsers and Node.js releases, but not Internet Explorer.

For TypeScript, version 5.4 or newer is required. Legacy decorators remain supported, and the README points at the installation guide for the required compiler configuration and the MobX 7 compatibility note, which is the practical reference for the decorator setup, since the standard and legacy decorator syntaxes need different tsconfig options.

The collaborative editing integrations are separate packages, mobx-keystone-yjs and mobx-keystone-loro, with complete examples in the online docs. The repository's package.json confirms the shape: yjs at 13.6.33 and loro-crdt at 1.16.3 are root dependencies, and there are separate build and test scripts for the yjs and loro libraries, so the CRDT bindings are first-class packages rather than examples.

What the feature list commits to

The reasons teams choose it section is short, and each item maps to a capability rather than a slogan.

Mutable action code with protected updates, so state changes stay explicit and safe. That is the two halves of the design in one line: you write mutations directly, and the protection is enforced rather than conventional.

Runtime snapshots and JSON patches for persistence, sync, replay, and debugging. The four uses are telling, because they are four different reasons to want the same artefact. Persistence stores a snapshot, sync sends patches, replay re-runs the action log, and debugging shows you what changed.

Built-in primitives for references, transactions, action middlewares, and undo/redo. References let one model point at another without duplicating state, transactions group changes so a snapshot is consistent across several of them, action middlewares are the hook for cross-cutting behaviour, and undo/redo is built on the action log.

Strong TypeScript inference for models, snapshots, and actions is the one that changes day to day work, because the snapshot type and the action type are derived from the model rather than declared separately and drifting.

Composable domain models that stay maintainable as app complexity grows, and close integration with MobX and mobx-react-lite. The last one is about the rendering layer: mobx-keystone does not replace React bindings, it plugs into them.

There are also two testimonials quoted from GitHub issues, and one of them captures the positioning well: it is described as taking the best bits from mobx and mobx-state-tree and putting them into a single package. The MST migration guide is linked in the quick links, which is the clearest evidence of where the audience comes from.

Where mobx-keystone fits, and where it stops

The comparison worth making is with plain MobX, with mobx-state-tree, and with Redux.

Against plain MobX, the difference is everything MobX deliberately leaves out. MobX gives you observables, actions and computed values, and no history, no persistence, no serialisation format, no protection against a stray assignment. mobx-keystone is MobX plus a defined tree structure, plus snapshots, patches and undo, plus the type system that ties them together. If your app never needs to serialise its state, send it anywhere, or let the user undo, plain MobX is less to learn.

Against mobx-state-tree, the difference is the day to day developer experience and the TypeScript story. MST is capable and has a reputation for ceremony, and mobx-keystone is positioned as taking the good parts with first-class TypeScript inference and MobX ergonomics. The cost is that the API is different, so migrating an existing MST codebase is a real project rather than a dependency swap, which is why the migration guide is linked as a first-class document in the quick links rather than buried.

Against Redux, the difference is the shape of the state. Redux is a reducer and a store, with a serialisable state tree and explicit actions, and mobx-keystone is model-driven with mutable code and derived snapshots. The README says the snapshot and middleware system makes it possible to replace a Redux reducer and store pair with model-driven state and connect Redux devtools, so the two are not equivalent, they just live in different idioms. Redux is also framework agnostic, which matters if you have a non-React surface, and mobx-keystone is React and MobX shaped.

The limits are structural. The library needs modern JavaScript, so Internet Explorer is out, MobX 4 is supported only with gotchas, and TypeScript 5.4 or newer is a floor. The decorator setup in a TypeScript project is a known friction point that the installation guide exists to address, and legacy decorators remain supported, which means you may be maintaining two decorator configurations.

The collaborative editing support is real but it comes as separate CRDT packages, so adopting collaborative editing means adding Yjs or Loro to your dependency tree and learning that binding rather than getting it from the core.

On maintenance, the last push was on 2026-10-11 with the repository not archived, there are no GitHub releases, and the version is carried in the package.json rather than in tags. The repository root has a CHANGELOG.md, a TODO.md, a pnpm workspace, an llms.txt, and an apps/ directory holding the documentation site, which is built with Netlify and vite-plus.

Editorial conclusion

mobx-keystone suits a TypeScript team that wants MobX style mutable model code and also needs undo, persistence, sync or replay from the same state tree, since the snapshot and patch machinery is the reason to use it rather than plain MobX. It is the wrong tool if you want a Redux reducer and store pair, because the model-driven approach replaces that shape rather than wrapping it, and it is the wrong tool for server rendering that assumes a single serialisable store. Before adopting it, read the MST migration guide, since a large part of the audience arrives from mobx-state-tree, and check the requirements, because MobX 4 is supported only with its gotchas and TypeScript 5.4 or newer is needed.

Frequently asked questions

How do I install mobx-keystone?

Install it alongside MobX, which is a peer dependency: npm install mobx mobx-keystone, or the yarn or pnpm equivalents. TypeScript 5.4 or newer is required, and the installation guide covers the decorator compiler configuration and the MobX 7 compatibility note.

What does mobx-keystone add on top of MobX?

A living tree of mutable but strictly protected models, from which immutable structurally shared snapshots are derived automatically. On top of that it provides JSON patches, undo and redo, action middlewares, transactions, references, runtime protection and CRDT bindings for collaborative editing through the mobx-keystone-yjs and mobx-keystone-loro packages.

How are models updated in mobx-keystone?

Through actions, and trees can only be modified by actions that belong to the same subtree. The methods are marked with the modelAction decorator and the fields are assigned directly inside them, and because the action log is replayable, changes can also be distributed or observed as fine-grained JSON patches.

Can mobx-keystone replace a Redux store?

The README says the snapshot and middleware system makes it possible to replace a Redux reducer and store pair with model-driven state and connect Redux devtools. The state model is different though, so it is a migration rather than a drop-in, and mobx-keystone remains shaped around MobX and mobx-react-lite.

Does mobx-keystone support collaborative editing?

Yes, through the mobx-keystone-yjs and mobx-keystone-loro CRDT bindings, with complete examples in the online docs. The repository's package.json lists yjs and loro-crdt as root dependencies and has separate build and test scripts for each binding library.

Official sources

  1. Issues
  2. License: MIT
  3. Project website
  4. README
  5. xaviergonz/mobx-keystone on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/xaviergonz-mobx-keystone.svg)](https://hysenlabs.com/projects/xaviergonz-mobx-keystone)

Was this useful?