Snapshots
Overview
Snapshots are immutable, structurally shared, representations of tree nodes (models and their children).
Snapshots in mobx-keystone
mainly serve these two purposes:
- As a serialization / deserialization mechanism (be it to store it or send it over the wire).
- As a way to bridge data to non-
mobx-react
-enabled React components.
Basically, when a change is performed over a tree node then a new immutable snapshot of it will be generated. Additionally, immutable snapshots for all parents will be generated as well. Any unchanged objects however will keep their snapshots unmodified.
For example, imagine a model A
with two children (B
and C
), and let's call their initial snapshots sA[0]
, sB[0]
and sC[0]
.
A -> sA[0] = getSnapshot(A)
B -> sB[0] = getSnapshot(B)
C -> sC[0] = getSnapshot(C)
If we change a property in B
then a new snapshot will be generated for it, as well as for all its parents (A
), but not for unaffected objects (C
in this case), thus resulting in:
A -> sA[1] = getSnapshot(A)
B -> sB[1] = getSnapshot(B)
C -> sC[0] = getSnapshot(C)
This means, as mentioned before, that snapshots generation is automatically optimized to only change their references when the objects they represent (and their children) actually change.
Note: Never change the contents of a snapshot object returned by getSnapshot
directly, clone it first! If you do weird things might happen.
Getting the snapshot of an instance
getSnapshot
getSnapshot<T>(value: T): SnapshotOutOf<T>
Getting the snapshot out of any tree node is as easy as this:
@model("myApp/Todo")
class Todo extends Model({
done: prop(false),
text: prop<string>()
}) {
}
const todo = new Todo({ text: "buy some milk" })
const todoSnapshot = getSnapshot(todo)
// this returns an object like
{
done: false,
text: "buy some milk",
$modelType: "myApp/Todo"
}
The additional $modelType
property is used to allow fromSnapshot
to recognize the original class and faithfully recreate it, rather than assume it is a plain object.
This metadata is only required for models, in other words, arrays, plain objects and primitives don't have this extra field.
The type returned by getSnapshot
is strongly typed, and is SnapshotOutOf<Todo>
in this case, which in this particular case evaluates as:
type SnapshotOutOf<Todo> = {
done: boolean
type: string
$modelType: string
}
Note that getSnapshot
can actually be used over any tree nodes (any model, or any plain object or array as long as at any point in time they become attached to a model or they are manually transformed into one via toTreeNode
), as well as primitives (though in the case of primitives the primitive will be returned directly).
Turning a snapshot back into an instance
fromSnapshot
fromSnapshot<T>(sn: SnapshotInOf<T>, options?: FromSnapshotOptions): T
// or typed
fromSnapshot<TType>(type: TType, sn: SnapshotInOf<TypeToData<TType>>, options?: FromSnapshotOptions): TypeToData<TType>
Restoring a snapshot is pretty easy as well:
const todo = fromSnapshot<Todo>(todoSnapshot)
// or typed
const todo = fromSnapshot(Todo, todoSnapshot)
The type accepted by fromSnapshot
is strongly typed as well, and is SnapshotInOf<Todo>
in this case, which in this particular case evaluates as:
type SnapshotInOf<Todo> = {
done?: boolean
type: string
$modelType: string
}
Compared to the output snapshot note how done
is now marked as optional since we declared a default value for it.
As for the options object, these options are available:
generateNewIds: boolean
- Passtrue
to generate new internal IDs for models rather than reusing them (default isfalse
).
Reacting to snapshot changes
Snapshots are observable values in themselves, which means standard MobX reactions such as this one work:
const disposer = reaction(
() => getSnapshot(todo),
(todoSnapshot) => {
// do something
}
)
onSnapshot
onSnapshot<T extends object>(obj: T | () => T, listener: (sn: SnapshotOutOf<T>, prevSn: SnapshotOutOf<T>) => void): () => void
Since that is a very common pattern, mobx-keystone
offers an onSnapshot
function that will call a listener with the new snapshot and the previous snapshot every time it changes.
const disposer = onSnapshot(todo, (newSnapshot, previousSnapshot) => {
// do something
})
In both cases the returned disposer function can be called to cancel the effect.
Applying snapshots
applySnapshot
applySnapshot<T extends object>(obj: T, sn: SnapshotInOf<T> | SnapshotOutOf<T>): void
It is also possible to apply a snapshot over an object, reconciling the contents of the object and therefore ensuring that only the minimal set of snapshot changes / patches is triggered:
// given `todo` is a todo with `{ done: false, text: "buy some milk" }`
applySnapshot(todo, {
done: true,
text: "buy some milk",
$modelType: todo.$modelType,
})
In the case above, only a single patch will be generated (for the done
property), and the same todo instance will be reused (since they have the same model ID).
Cloning via snapshots
clone
clone<T extends object>(value: T, options?: CloneOptions): T
Snapshots can also be used to clone values. clone
is just sugar syntax around getSnapshot
and fromSnapshot
with generateNewIds
set to true
by default.
const clonedTodo = clone(todo)