MobX-state-tree

MobX-state-tree

  • Documentation
  • TypeDocs
  • Sponsor
  • GitHub

›API Overview

Introduction

  • Welcome to MobX-State-Tree!
  • Installation
  • Getting Started Tutorial
  • Examples
  • Overview & Philosophy

Basic Concepts

  • Types, models, trees & state
  • Actions
  • Derived values
  • React and MST
  • Snapshots
  • Identifiers and references
  • Asynchronous actions

Advanced Concepts

  • Patches
  • Listening to changes
  • Dependency Injection
  • Middleware
  • Reconciliation
  • Volatile state

API Overview

  • Types overview
  • API overview
  • Lifecycle hooks overview

Tips

  • Talks & Blogs
  • Frequently Asked Questions
  • TypeScript and MST
  • Circular dependencies
  • Simulating inheritance
  • Using snapshots as values
  • Miscellaneous Tips

Compare

  • React Context vs. MobX-State-Tree

Recipes

  • Auto-Generated Property Setter Actions
  • Pre-built Form Types with MST Form Type
  • Manage Asynchronous Data with mst-query
Edit

Lifecycle hooks overview

egghead.io lesson 14: Loading Data from the Server after model creation
Hosted on egghead.io

mobx-state-tree supports passing a variety of hooks that are called throughout a node's lifecycle. Hooks are passes as actions with the name of the hook, like:

const Todo = types.model("Todo", { done: true }).actions((self) => ({
  afterCreate() {
    console.log("Created a new todo!")
  }
}))
HookMeaning
afterCreateImmediately after an instance is created and initial values are applied. Children will fire this event before parents. You can't make assumptions about the parent safely, use afterAttach if you need to.
afterAttachAs soon as the direct parent is assigned (this node is attached to another node). If an element is created as part of a parent, afterAttach is also fired. Unlike afterCreate, afterAttach will fire breadth first. So, in afterAttach one can safely make assumptions about the parent, but in afterCreate not
beforeDetachAs soon as the node is removed from the direct parent, but only if the node is not destroyed. In other words, when detach(node) is used
beforeDestroyCalled before the node is destroyed, as a result of calling destroy, or by removing or replacing the node from the tree. Child destructors will fire before parents
preProcessSnapshotDeprecated, prefer types.snapshotProcessor. Before creating an instance or applying a snapshot to an existing instance, this hook is called to give the option to transform the snapshot before it is applied. The hook should be a pure function that returns a new snapshot. This can be useful to do some data conversion, enrichment, property renames, etc. This hook is not called for individual property updates. **Note 1: Unlike the other hooks, this one is _not created as part of the actions initializer, but directly on the type!_ _Note 2: The preProcessSnapshot transformation must be pure; it should not modify its original input argument!**_
postProcessSnapshotDeprecated, prefer types.snapshotProcessor. This hook is called every time a new snapshot is being generated. Typically it is the inverse function of preProcessSnapshot. This function should be a pure function that returns a new snapshot. **Note: Unlike the other hooks, this one is _not created as part of the actions initializer, but directly on the type!**_

All hooks can be defined multiple times and can be composed automatically.

Lifecycle hooks for types.array/types.map

Hooks for types.array/types.map can be defined by using the .hooks(self => ({})) method.

Calling .hooks(...) produces new type, same as calling .actions() for types.model.

Available hooks are:

HookMeaning
afterCreateImmediately after an instance is initialized: right after .create() for root node or after the first access for the nested one. Children will fire this event before parents. You can't make assumptions about the parent safely, use afterAttach if you need to.
afterAttachAs soon as the direct parent is assigned (this node is attached to another node). If an element is created as part of a parent, afterAttach is also fired. Unlike afterCreate, afterAttach will fire breadth first. So, in afterAttach one can safely make assumptions about the parent, but in afterCreate not
beforeDetachAs soon as the node is removed from the direct parent, but only if the node is not destroyed. In other words, when detach(node) is used
beforeDestroyCalled before the node is destroyed, as a result of calling destroy, or by removing or replacing the node from the tree. Child destructors will fire before parents

Snapshot processing hooks

You can also modify snapshots as they are generated from your nodes, or applied to your nodes with types.snapshotProcessor. This type wraps an existing type and allows defining custom hooks for snapshot modifications.

For example, you can wrap an existing model in a snapshot processor which transforms a snapshot from the server into the shape your model expects with preProcess:

const TodoModel = types.model("Todo", {
  done: types.boolean,
});

const Todo = types.snapshotProcessor(TodoModel, {
  preProcessor(snapshot) {
    return {
        // auto convert strings to booleans as part of preprocessing
        done: snapshot.done === "true" ? true : snapshot.done === "false" ? false : snapshot.done
    }
});

const todo = Todo.create({ done: "true" }) // snapshot will be transformed on the way in

Snapshots can also be transformed from the base shape generated by mobx-quick-tree using the postProcess hook. For example, we can format a date object in the snapshot with a specific date format that a backend might accept:

const TodoModel = types.model("Todo", {
  done: types.boolean,
  createdAt: types.Date
});

const Todo = types.snapshotProcessor(TodoModel, {
  postProcessor(snapshot, node) {
    return {
        ...snapshot,
        createdAt: node.createdAt.getTime()
    }
});

const todo = Todo.create({done: true, createdAt: new Date()});
const snapshot = getSnapshot(todo);
// { done: true, createdAt: 1699504649386 }
HookMeaning
preProcessor(inputSnapshot)Transform a snapshot before it is applied to a node. The output snapshot must be valid for application to the wrapped type. The preProcess hook is passed the input snapshot, but not passed the node, as it is not done being constructed yet, and not attached to the tree. If you need to modify the node in the context of the tree, use the afterCreate hook.
postProcessor(outputSnapshot, node)Transform a snapshot after it has been generated from a node. The transformed value will be returned by getSnapshot. The postProcess hook is passed the initial outputSnapshot, as well as the instance object the snapshot has been generated for. It is safe to access properties of the node or other nodes when post processing snapshots.

When to use snapshot hooks

preProcess and postProcess hooks should be used to convert your data into types that are more acceptable to MST. Snapshots are often JSON serialized, so if you need to use richly typed objects like URLs or Dates that can't be JSON serialized, you can use snapshot processors to convert to and from the serialized form.

Typically, it should be the case that postProcessor(preProcessor(snapshot)) === snapshot. If your snapshot processor hooks are non-deterministic, or rely on state beyond just the base snapshot, it's easy to introduce subtle bugs and is best avoided.

If you are considering adding a snapshot processor that is non-deterministic or relies on other state, consider using a dedicated property or view that produces the same information. Like snapshots, properties and views are observable and memoized, but they don't need to have an inverse for serializing back to a snapshot.

For example, if you want to capture the current time a snapshot was generated, you may be tempted to use a snapshot processor:

const TodoModel = types.model("Todo", {
  done: types.boolean,
});

const Todo = types.snapshotProcessor(TodoModel, {
  // discouraged, try not to do this
  postProcessor(snapshot, node) {
    return {
        ...snapshot,
        createdAt: new Date().toISOString();
    }
});

const todo = Todo.create({ done: false })
getSnapshot(todo) // will have a `createdAt property`

Instead, this data could be better represented as a property right on the model, which is included in the snapshot by default:

const Todo = types.model("Todo", {
  done: types.boolean,
  createdAt: types.optional(types.Date, () => new Date())
});

const todo = Todo.create({ done: false })
getSnapshot(todo) // will also have a `createdAt property`

Advanced use-cases that require impure or otherwise inconsistent snapshot processors are however supported by MST.

← API overviewTalks & Blogs →
  • Lifecycle hooks for types.array/types.map
    • Snapshot processing hooks
MobX-state-tree
Docs
Getting StartedBasic conceptsAdvanced conceptsAPI Reference
Community
GitHub DiscussionsStack Overflow
More
BlogGitHubStar
Facebook Open Source
Copyright © 2025 Michel Weststrate