Loro Binding (mobx-keystone-loro)
The mobx-keystone-loro package keeps a Loro document in sync with a mobx-keystone store and vice versa. This is useful when multiple clients need to stay in sync through peer-to-peer networking, an intermediate server, or another transport. Since Loro is a CRDT, concurrent edits remain conflict-free, which makes optimistic updates practical even when clients go offline temporarily.
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,
})
Note that the loroObject must be a LoroMap, LoroMovableList or a LoroText and its structure must be compatible with the provided mobx-keystone type (or, to be more precise, its snapshot form).
First migration - converting JSON to Loro data
If you already have a model instance snapshot stored somewhere and want to start binding it to a Loro data structure you can use the convertJsonToLoroData function to make this first migration.
convertJsonToLoroData takes a single argument, a JSON value (usually a snapshot of the model you want to bind) and returns a Loro data structure (LoroMap, LoroMovableList, etc.) ready to be bound to that model. Frozen values are a special case and they are kept as 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.