Security News
The Risks of Misguided Research in Supply Chain Security
Snyk's use of malicious npm packages for research raises ethical concerns, highlighting risks in public deployment, data exfiltration, and unauthorized testing.
mobx-state-tree
Advanced tools
Opinionated, transactional, MobX powered state container
An introduction to the philosophy can be watched here. Slides. Or, as markdown to read it quickly.
NPM:
npm install mobx-state-tree --save-dev
CDN:
https://unpkg.com/mobx-state-tree/mobx-state-tree.umd.js
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:
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).
mobx-state-tree
supports JSON patches, replayable actions, listeners for patches, actions and snapshots. References, maps, arrays. Just read on :)Models are at the heart of mobx-state-tree
. They simply store your data.
mobx
concept of computed
values.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 factoryclone(model)
: constructs a deep clone of the given model instanceA 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.
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 modelonSnapshot(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 snapshotActions modify models. Actions are replayable and are therefore constrained in several ways:
A serialized action call looks like:
{
name: "setAge"
path: "/user",
args: [17]
}
Useful methods:
name: function(/* args */) { /* body */ }
(ES5) or name (/* args */) { /* body */ }
(ES6) to construct actionsonAction(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 descriptionIt 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.
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 and references are two powerful abstraction that work well together.
identifier()
propertiesarray
or map
)map.put()
method can be used to simplify adding objects to maps that have identifiersExample:
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 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).
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.
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
}
path
attribute of a patch considers the relative path of the event from the place where the event listener is attachedUseful 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 mutatedapplyPatch(model, patch)
applies a patch to the provided modelSee #10
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
lib/types/complex-types/complex-type.js:18-48
A complex type produces a MST node (Node in the state tree)
lib/core/mst-node-administration.js:48-52
Returnes (escaped) path representation as string
Parameters
subFactory
[ModelFactory] (optional, default primitiveFactory
)Parameters
subFactory
[ModelFactory] (optional, default primitiveFactory
)lib/types/complex-types/object.js:41-41
Parsed description of all properties
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 onmiddleware
Returns IDisposer function to remove the middleware
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 patchescallback
Returns IDisposer function to remove the listener
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
target
Objectpatch
IJsonPatchlib/core/mst-operations.js:94-99
Applies a number of JSON patches in a single MobX transaction
Parameters
lib/core/mst-operations.js:125-129
Applies a series of actions in a single MobX transaction.
Does not return any value
Parameters
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
lib/core/mst-operations.js:170-172
Returns true if the object is in protected mode, @see protect
Parameters
target
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
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
Returns boolean
lib/core/mst-operations.js:234-236
Returns the path of the given object in the model tree
Parameters
target
ObjectReturns string
lib/core/mst-operations.js:245-247
Returns the path of the given object as unescaped string array
Parameters
target
Objectlib/core/mst-operations.js:256-258
Returns true if the given object is the root of a model tree
Parameters
target
ObjectReturns boolean
lib/core/mst-operations.js:268-272
Resolves a path relatively to a given object.
Parameters
Returns Any
lib/core/mst-operations.js:282-287
Parameters
Returns Any
lib/core/mst-operations.js:301-310
Parameters
source
TkeepEnvironment
Returns T
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
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
Dispatches an Action on a model instance. All middlewares will be triggered. Returns the value of the last actoin
Parameters
target
Objectaction
IActionCalloptions
[IActionCallOptions]escape slashes and backslashes http://tools.ietf.org/html/rfc6901
Parameters
str
unescape slashes and backslashes
Parameters
str
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.
Some model constructions which are supported by mobx are not supported by mobx-state-tree
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 registerySo far this might look a lot like an immutable state tree as found for example in Redux apps, but there are a few differences:
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 }
0.3.2
FAQs
Opinionated, transactional, MobX powered state container
The npm package mobx-state-tree receives a total of 70,255 weekly downloads. As such, mobx-state-tree popularity was classified as popular.
We found that mobx-state-tree demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 8 open source maintainers collaborating on the project.
Did you know?
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.
Security News
Snyk's use of malicious npm packages for research raises ethical concerns, highlighting risks in public deployment, data exfiltration, and unauthorized testing.
Research
Security News
Socket researchers found several malicious npm packages typosquatting Chalk and Chokidar, targeting Node.js developers with kill switches and data theft.
Security News
pnpm 10 blocks lifecycle scripts by default to improve security, addressing supply chain attack risks but sparking debate over compatibility and workflow changes.