Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

mobx-state-tree

Package Overview
Dependencies
Maintainers
2
Versions
129
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mobx-state-tree

Opinionated, transactional, MobX powered state container

  • 0.3.2
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
85K
increased by72%
Maintainers
2
Weekly downloads
 
Created
Source

mobx-state-tree

This package is work in progress, stay tuned

Opinionated, transactional, MobX powered state container

Build Status Coverage Status Join the chat at https://gitter.im/mobxjs/mobx

An introduction to the philosophy can be watched here. Slides. Or, as markdown to read it quickly.

Installation

NPM:

npm install mobx-state-tree --save-dev

CDN:

https://unpkg.com/mobx-state-tree/mobx-state-tree.umd.js

Philosophy

mobx-state-tree is a state container that combines the simplicity and ease of mutable data with the traceability of immutable data and the reactiveness and performance of observable data.

It is an opt-in state container that can be used in MobX, but also Redux based applications.

If MobX is like a spreadsheet mechanism for javascript, then mobx-state-tree is like storing your spreadsheet in git.

Unlike MobX itself, mobx-state-tree is quite opinionated on how you structure your data. This makes it possible to solve many problems generically and out of the box, like:

  • (De-) serialization
  • Snapshotting state
  • Replaying actions
  • Time travelling
  • Emitting and applying JSON patches
  • Protecting state against uncontrolled mutations
  • Using middleware
  • Using dependency injection
  • Maintaining invariants

mobx-state-tree tries to take the best features from both object oriented (discoverability, co-location and encapsulation), and immutable based state management approaches (transactionality, sharing functionality through composition).

Concepts

  1. The state is represented as a tree of models.
  2. models are created using factories.
  3. A factory basically takes a snapshot and a clone of a base model and copies the two into a fresh model instance.
  4. A snapshot is the immutable representation of the state of a model. In other words, a one-time copy of the internal state of a model at a certain point in time.
  5. snapshots use structural sharing. So a snapshot of a node in the tree is composed of the snapshots of it's children, where unmodified snapshots are always shared
  6. mobx-state-tree supports JSON patches, replayable actions, listeners for patches, actions and snapshots. References, maps, arrays. Just read on :)

Models

Models are at the heart of mobx-state-tree. They simply store your data.

  • Models are self-contained.
  • Models have fields. Either primitive or complex objects like maps, arrays or other models. In short, these are MobX observables. Fields can only be modified by actions.
  • Models have derived fields. Based on the mobx concept of computed values.
  • Models have actions. Only actions are allowed to change fields. Fields cannot be changed directly. This ensures replayability of the application state.
  • Models can contain other models. However, models are not allowed to form a graph (using direct references) but must always have a tree shape. This enables many feature like standardized serialization and cloning.
  • Models can be snapshotted at any time
  • Models can be created using factories, that take copy a base model and combine it with a (partial) snapshot

Example:

import { types } from "mobx-state-tree"
import uuid from "uuid"

const Box = types.model("Box",{
    // props
    id: types.identifier(),
    name: "",
    x: 0,
    y: 0,

    // computed prop
    get width() {
        return this.name.length * 15
    },

    // action
    move(dx, dy) {
        this.x += dx
        this.y += dy
    }
})

const BoxStore = types.model("BoxStore",{
    boxes: types.map(Box),
    selection: types.reference("boxes/name"),
    addBox(name, x, y) {
        const box = Box.create({ id: uuid(), name, x, y })
        this.boxes.put(box)
        return box
    }
})

const boxStore = BoxStore.create({
    "boxes": {},
    "selection": ""
});
const box = boxStore.addBox("test",100,100)
box.move(7, 3)

Useful methods:

  • types.model(exampleModel): creates a new factory
  • clone(model): constructs a deep clone of the given model instance

Snapshots

A snapshot is a representation of a model. Snapshots are immutable and use structural sharing (sinces model can contain models, snapshots can contain other snapshots). This means that any mutation of a model results in a new snapshot (using structural sharing) of the entire state tree. This enables compatibility with any library that is based on immutable state trees.

  • Snapshots are immutable
  • Snapshots can be transported
  • Snapshots can be used to update / restore models to a certain state
  • Snapshots use structural sharing
  • It is posible to subscribe to models and be notified of each new snapshot
  • Snapshots are automatically converted to models when needed. So assignments like boxStore.boxes.set("test", Box({ name: "test" })) and boxStore.boxes.set("test", { name: "test" }) are both valid.

Useful methods:

  • getSnapshot(model): returns a snapshot representing the current state of the model
  • onSnapshot(model, callback): creates a listener that fires whenever a new snapshot is available (but only one per MobX transaction).
  • applySnapshot(model, snapshot): updates the state of the model and all its descendants to the state represented by the snapshot

Actions

Actions modify models. Actions are replayable and are therefore constrained in several ways:

  • Actions can be invoked directly as method on a model
  • All action arguments must be serializable. Some arguments can be serialized automatically, such as relative paths to other nodes
  • Actions are serializable and replayable
  • It is possible to subscribe to the stream of actions that is invoked on a model
  • Actions can only modify models that belong to the tree on which they are invoked
  • Actions are automatically bound the their instance, so it is save to pass actions around first class without binding or wrapping in arrow functions.

A serialized action call looks like:

{
   name: "setAge"
   path: "/user",
   args: [17]
}

Useful methods:

  • Use name: function(/* args */) { /* body */ } (ES5) or name (/* args */) { /* body */ } (ES6) to construct actions
  • onAction(model, middleware) listens to any action that is invoked on the model or any of it's descendants. See onAction for more details.
  • applyAction(model, action) invokes an action on the model according to the given action description

It is not necessary to express all logic around models as actions. For example it is not possible to define constructors on models. Rather, it is recommended to create stateless utility methods that operate on your models. It is recommended to keep models self-contained and to do orchestration around models in utilities around it.

Protecting the state tree

By default it is allowed to both directly modify a model or through an action. However, in some cases you want to guarantee that the state tree is only modified through actions. So that replaying action will reflect everything that can possible have happened to your objects, or that every mutation passes through your action middleware etc. To disable modifying data in the tree without action, simple call protect(model). Protect protects the passed model an all it's children

const Todo = types.model({
    done: false,
    toggle() {
        this.done = !this.done
    }
})

const todo = new Todo()
todo.done = true // OK
protect(todo)
todo.done = false // throws!
todo.toggle() // OK

Identifiers

Identifiers and references are two powerful abstraction that work well together.

  • Each model can define zero or one identifier() properties
  • The identifier property of an object cannot be modified after initialization
  • Identifiers should be unique within their parent collection (array or map)
  • Identifiers are used to reconcile items inside arrays and maps wherever possible when applying snapshots
  • The map.put() method can be used to simplify adding objects to maps that have identifiers

Example:

const Todo = types.model({
    id: types.identifier(),
    title: "",
    done: false
})

const todo1 = Todo.create() // not ok, identifier is required
const todo1 = Todo.create({ id: "1" }) // ok
applySnapshot(todo1, { id: "2", done: false}) // not ok; cannot modify the identifier of an object

const store = types.map(Todo)
store.put(todo1) // short-hand for store.set(todo1.id, todo)

References

References can be used to refer to link to an arbitrarily different object in the tree transparently. This makes it possible to use the tree as graph, while behind the scenes the graph is still properly serialized as tree

Example:

const Store = types.model({
    selectedTodo: types.reference(Todo),
    todos: types.array(Todo)
})

const store = Store({ todos: [ /* some todos */ ]})

store.selectedTodo = store.todos[0] // ok
store.selectedTodo === store.todos[0] // true
getSnapshot(store) // serializes properly as tree: { selectedTodo: { $ref: "../todos/0" }, todos: /* */ }

store.selectedTodo = Todo() // not ok; have to refer to something already in the same tree

By default references can point to any arbitrary object in the same tree (as long as it has the proper type).

References with predefined resolve paths

It is also possible to specifiy in which collection the reference should resolve by passing a second argument, the resolve path (this can be relative):

const Store = types.model({
    selectedTodo: types.reference(Todo, "/todos/"),
    todos: types.array(Todo)
})

If a resolve path is provided, reference no longer stores a json pointer, but pinpoints the exact object that is being referred to by it's identifier. Assuming that Todo specified an identifier() property:

getSnapshot(store) // serializes tree: { selectedTodo: "17" /* the identifier of the todo */, todos: /* */ }

The advantage of this approach is that paths are less fragile, where default references serialize the path by for example using array indices, an identifier with a resolve path will find the object by using it's identifier.

Utility methods

  • No restriction in arguments and return types
  • Cannot modify data except though actions

Patches

Modifying a model does not only result in a new snapshot, but also in a stream of JSON-patches describing which modifications are made. Patches have the following signature:

export interface IJsonPatch {
    op: "replace" | "add" | "remove"
    path: string
    value?: any
}
  • Patches are constructed according to JSON-Patch, RFC 6902
  • Patches are emitted immediately when a mutation is made, and don't respect transaction boundaries (like snapshots)
  • Patch listeners can be used to achieve deep observing
  • The path attribute of a patch considers the relative path of the event from the place where the event listener is attached
  • A single mutation can result in multiple patches, for example when splicing an array

Useful methods:

  • onPatch(model, listener) attaches a patch listener to the provided model, which will be invoked whenever the model or any of it's descendants is mutated
  • applyPatch(model, patch) applies a patch to the provided model

Be careful with direct references to items in the tree

See #10

Factory composition

Tree semantics

Single or multiple state

Using mobx and mobx-state-tree together

Integrations

Examples

Environments

API

maybeMST

lib/core/mst-node.js:28-40

Tries to convert a value to a TreeNode. If possible or already done, the first callback is invoked, otherwise the second. The result of this function is the return value of the callbacks, or the original value if the second callback is omitted

Parameters

  • value
  • asNodeCb
  • asPrimitiveCb

ComplexType

lib/types/complex-types/complex-type.js:18-48

A complex type produces a MST node (Node in the state tree)

get

lib/core/mst-node-administration.js:48-52

Returnes (escaped) path representation as string

map

lib/types/index.js:24-26

Parameters

  • subFactory [ModelFactory] (optional, default primitiveFactory)

array

lib/types/index.js:34-36

Parameters

  • subFactory [ModelFactory] (optional, default primitiveFactory)

props

lib/types/complex-types/object.js:41-41

Parsed description of all properties

addMiddleware

lib/core/mst-operations.js:50-55

TODO: update docs Registers middleware on a model instance that is invoked whenever one of it's actions is called, or an action on one of it's children. Will only be invoked on 'root' actions, not on actions called from existing actions.

The callback receives two parameter: the action parameter describes the action being invoked. The next() function can be used to kick off the next middleware in the chain. Not invoking next() prevents the action from actually being executed!

Action calls have the following signature:

export type IActionCall = {
   name: string;
   path?: string;
   args?: any[];
}

Example of a logging middleware:

function logger(action, next) {
  console.dir(action)
  return next()
}

onAction(myStore, logger)

myStore.user.setAge(17)

// emits:
{
   name: "setAge"
   path: "/user",
   args: [17]
}

Parameters

  • target Object model to intercept actions on
  • middleware

Returns IDisposer function to remove the middleware

onPatch

lib/core/mst-operations.js:67-69

Registers a function that will be invoked for each that as made to the provided model instance, or any of it's children. See 'patches' for more details. onPatch events are emitted immediately and will not await the end of a transaction. Patches can be used to deep observe a model tree.

Parameters

  • target Object the model instance from which to receive patches
  • callback

Returns IDisposer function to remove the listener

applyPatch

lib/core/mst-operations.js:83-85

Applies a JSON-patch to the given model instance or bails out if the patch couldn't be applied

Parameters

applyPatches

lib/core/mst-operations.js:94-99

Applies a number of JSON patches in a single MobX transaction

Parameters

applyActions

lib/core/mst-operations.js:125-129

Applies a series of actions in a single MobX transaction.

Does not return any value

Parameters

  • target Object
  • actions Array<IActionCall>
  • options [IActionCallOptions]

protect

lib/core/mst-operations.js:163-165

By default it is allowed to both directly modify a model or through an action. However, in some cases you want to guarantee that the state tree is only modified through actions. So that replaying action will reflect everything that can possible have happened to your objects, or that every mutation passes through your action middleware etc. To disable modifying data in the tree without action, simple call protect(model). Protect protects the passed model an all it's children

Parameters

  • target

Examples

const Todo = types.model({
    done: false,
    toggle() {
        this.done = !this.done
    }
})

const todo = new Todo()
todo.done = true // OK
protect(todo)
todo.done = false // throws!
todo.toggle() // OK

isProtected

lib/core/mst-operations.js:170-172

Returns true if the object is in protected mode, @see protect

Parameters

  • target

applySnapshot

lib/core/mst-operations.js:182-184

Applies a snapshot to a given model instances. Patch and snapshot listeners will be invoked as usual.

Parameters

hasParent

lib/core/mst-operations.js:198-208

Given a model instance, returns true if the object has a parent, that is, is part of another object, map or array

Parameters

  • target Object
  • depth number = 1, how far should we look upward?

Returns boolean

getPath

lib/core/mst-operations.js:234-236

Returns the path of the given object in the model tree

Parameters

Returns string

getPathParts

lib/core/mst-operations.js:245-247

Returns the path of the given object as unescaped string array

Parameters

Returns Array<string>

isRoot

lib/core/mst-operations.js:256-258

Returns true if the given object is the root of a model tree

Parameters

Returns boolean

resolve

lib/core/mst-operations.js:268-272

Resolves a path relatively to a given object.

Parameters

Returns Any

tryResolve

lib/core/mst-operations.js:282-287

Parameters

Returns Any

clone

lib/core/mst-operations.js:301-310

Parameters

  • source T
  • keepEnvironment

Returns T

detach

lib/core/mst-operations.js:315-318

Removes a model element from the state tree, and let it live on as a new state tree

Parameters

  • thing

destroy

lib/core/mst-operations.js:323-327

Removes a model element from the state tree, and mark it as end-of-life; the element should not be used anymore

Parameters

  • thing

applyAction

lib/core/action.js:107-114

Dispatches an Action on a model instance. All middlewares will be triggered. Returns the value of the last actoin

Parameters

  • target Object
  • action IActionCall
  • options [IActionCallOptions]

escapeJsonPath

lib/core/json-patch.js:9-11

escape slashes and backslashes http://tools.ietf.org/html/rfc6901

Parameters

  • str

unescapeJsonPath

lib/core/json-patch.js:16-18

unescape slashes and backslashes

Parameters

  • str

FAQ

Should all state of my app be stored in mobx-state-tree? No, or, not necessarily. An application can use both state trees and vanilla MobX observables at the same time. State trees are primarily designed to store your domain data, as this kind of state is often distributed and not very local. For, for example, local component state, vanilla MobX observables might often be simpler to use.

No constructors?

Neh, replayability. Use utilities instead

No inheritance?

No use composition or unions instead.

Constraints

Some model constructions which are supported by mobx are not supported by mobx-state-tree

  • Data graphs are not supported, only data trees
  • This means that each object needs to uniquely contained
  • Only containment relations are allowed. Associations need to be expressed with 'foreign keys'; strings identifying other objects. However there is a standard pattern enabling using real objects as references with a little boilerplate, see working with associations.
  • mobx-state-tree does currently not support inheritance / subtyping. This could be changed by popular demand, but not supporting inheritance avoids the need to serialize type information or keeping a (global) type registery

Features

  • Provides immutable, structurally shared snapshots which can be used as serialization or for time travelling. Snapshots consists entirely of plain objects.
  • Provides JSON patch streams for easy remote synchronization or easy diffing.
  • Each object is uniquely contained and has an explicit path like in a file system. This enables using relative references and is very useful for debugging.
  • State trees are composable
  • There can be many state trees in a single app.

Comparison with immutable state trees

So far this might look a lot like an immutable state tree as found for example in Redux apps, but there are a few differences:

  • mobx-state-tree allow direct modification of any value in the tree, it is not needed to construct a new tree in your actions
  • mobx-state-tree allows for fine grained and efficient observability on any point in the state tree
  • mobx-state-tree generates json patches for any modification that is made
  • (?) mobx-state-tree is a valid redux store, providing the same api (TODO)

TypeScript & MST

When using models, you write interface along with it's property types that will be used to perform type checks at runtime. What about compile time? You can use TypeScript interfaces indeed to perform those checks, but that would require writing again all the properties and their actions! Good news? You don't need to write it twice! Using the typeof operator of TypeScript over the .Type property of a MST Type, will result in a valid TypeScript Type!

const Todo = types.model({
    title: types.string,
    setTitle(v: string) {
        this.title = v
    }
})
type ITodo = typeof Todo.Type // => ITodo is now a valid TypeScript type with { title: string; setTitle: (v: string) => void }

Keywords

FAQs

Package last updated on 24 Apr 2017

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc