Loro Binding (mobx-keystone-loro)
The mobx-keystone-loro package keeps a Loro document and a mobx-keystone store synchronized in both directions. Loro is a CRDT, so replicas can accept local changes while offline and merge concurrent edits when they reconnect. You supply the networking and persistence layer appropriate for your application.
Installation
Install the binding and its peer dependencies:
npm install mobx mobx-keystone loro-crdt mobx-keystone-loro
Binding Loro data to a model instance
const {
// The bound mobx-keystone instance.
boundObject,
// Disposes the binding.
dispose,
// The Loro origin string used for binding transactions.
loroOrigin,
} = bindLoroToMobxKeystone({
// The mobx-keystone model type.
mobxKeystoneType,
// The Loro document.
loroDoc,
// The bound Loro data structure.
loroObject,
})
The loroObject must be a LoroMap, LoroMovableList, or LoroText attached to loroDoc. Its current JSON representation must be a valid input snapshot for mobxKeystoneType. The returned boundObject is created from that CRDT data, and later changes flow in both directions until dispose() is called.
First migration - converting JSON to Loro data
If you already have a stored model snapshot, use convertJsonToLoroData to create the initial Loro structure before binding it.
convertJsonToLoroData accepts a JSON value (usually the model snapshot) and returns the corresponding Loro structure (LoroMap, LoroMovableList, and so on). Frozen values remain immutable plain values.
If you already have an existing LoroMap or LoroMovableList and want to copy JSON data into it, use the helper functions:
applyJsonObjectToLoroMap(dest, source, options?)applyJsonArrayToLoroMovableList(dest, source, options?)
These are useful when seeding or refreshing part of a document without rebuilding the container tree manually.
The optional options.mode controls how data is applied:
"add"- Create and insert new Loro containers from the JSON data. This is the default."merge"- Recursively merge into existingLoroMap/LoroMovableListcontainers when possible, preserving existing container references.
For example:
const loroRoot = doc.getMap("rootStore")
// First seed
applyJsonObjectToLoroMap(loroRoot, snapshot)
// Later, merge an updated snapshot while preserving existing nested containers
applyJsonObjectToLoroMap(loroRoot, nextSnapshot, { mode: "merge" })
Using LoroText as a model node
The special model LoroTextModel can be used to bind a LoroText to a mobx-keystone model.
const text = LoroTextModel.withText("Hello world!")
// once `text` is part of a bound tree:
text.insertText(0, "Say: ")
text.text // "Say: Hello world!"
Note that loroText returns undefined while the model is not part of a bound tree.
If you want to use LoroTextModel in a runtime type-checked property, loroTextModelType is the exported types.model(...) helper for that:
@model("myApp/Doc")
class DocModel extends Model({
text: tProp(loroTextModelType),
}) {}
When working with snapshots, isLoroTextModelSnapshot(value) can be useful to detect whether a snapshot represents a LoroTextModel.
Move Operations
Unlike Y.js, Loro's LoroMovableList supports native move operations. To take advantage of this, use the moveWithinArray helper function:
import { moveWithinArray } from "mobx-keystone-loro"
runUnprotected(() => {
// Move item from index 0 to index 3
moveWithinArray(boundObject.items, 0, 3)
})
moveWithinArray(array, fromIndex, toIndex) moves an item within an array:
- fromIndex: The current index of the item to move
- toIndex: The target index (position before the move happens)
When used on a mobx-keystone array bound to Loro, this translates to a native loroList.move() operation, preserving the identity and history of the moved item across all clients.
For unbound arrays, it performs a standard splice-based move.
The LoroBindingContext
All nodes inside a bound tree have access to a LoroBindingContext instance.
The instance can be accessed using:
loroBindingContext.get(nodePartOfTheBoundTree)
And this instance provides access to the following data:
loroDoc: TheLorodocument.loroObject: The boundLorodata structure.mobxKeystoneType: Themobx-keystonemodel type.loroOrigin: The origin string used for transactions.boundObject: The boundmobx-keystoneinstance.isApplyingLoroChangesToMobxKeystone: Whether we are currently applyingLorochanges to themobx-keystonemodel.
Example
A full example is available here.