Skip to main content

Y.js Binding Example

In this example we will be synchronizing two separate root stores via the mobx-keystone-yjs package (documentation here). This package ensures a Y.js document is kept in sync with a mobx-keystone store and vice-versa. This is useful for example when you when you want to have multiple clients that keep in sync with each other using a peer-to-peer connection, an intermediate server, etc. Another nice feature of CRDTs is that they are conflict-free, so you don't have to worry about conflicts when two clients edit the same data at the same time, even if they were offline while doing such changes. Due to this all updates become optimistic updates and perceived performance is great since it does not require a server confirming the operation.

Note: This example uses y-webrtc to share state using WebRTC (P2P).

Code

appInstance.tsx

examples/clientServer/appInstance.tsx
import { observable } from "mobx"
import { getSnapshot, registerRootStore } from "mobx-keystone"
import { applyJsonObjectToYMap, bindYjsToMobxKeystone } from "mobx-keystone-yjs"
import { observer } from "mobx-react"
import { useState } from "react"
import { WebrtcProvider } from "y-webrtc"
import * as Y from "yjs"
import { TodoListView } from "../todoList/app"
import { TodoList, createDefaultTodoList } from "../todoList/store"

function getInitialState() {
// here we just generate what a client would have saved from a previous session,
// but it could be stored in each client local storage or something like that

const yjsDoc = new Y.Doc()
const yjsRootStore = yjsDoc.getMap("rootStore")

const todoListSnapshot = getSnapshot(createDefaultTodoList())
applyJsonObjectToYMap(yjsRootStore, todoListSnapshot)

const updateV2 = Y.encodeStateAsUpdateV2(yjsDoc)

yjsDoc.destroy()

return updateV2
}

function initAppInstance() {
// we get the initial state from the server, which is a Yjs update
const rootStoreYjsUpdate = getInitialState()

// hydrate into a Yjs document
const yjsDoc = new Y.Doc()
Y.applyUpdateV2(yjsDoc, rootStoreYjsUpdate)

// and create a binding into a mobx-keystone object rootStore
const { boundObject: rootStore } = bindYjsToMobxKeystone({
mobxKeystoneType: TodoList,
yjsDoc,
yjsObject: yjsDoc.getMap("rootStore"),
})

// although not strictly required, it is always a good idea to register your root stores
// as such, since this allows the model hook `onAttachedToRootStore` to work and other goodies
registerRootStore(rootStore)

// connect to other peers via webrtc
const webrtcProvider = new WebrtcProvider("mobx-keystone-yjs-binding-demo", yjsDoc)

// expose the webrtc connection status as an observable
const status = observable({
connected: webrtcProvider.connected,
setConnected(value: boolean) {
this.connected = value
},
})

webrtcProvider.on("status", (event) => {
status.setConnected(event.connected)
})

const toggleWebrtcProviderConnection = () => {
if (webrtcProvider.connected) {
webrtcProvider.disconnect()
} else {
webrtcProvider.connect()
}
}

return { rootStore, status, toggleWebrtcProviderConnection }
}

export const AppInstance = observer(() => {
const [{ rootStore, status, toggleWebrtcProviderConnection }] = useState(() => initAppInstance())

return (
<>
<TodoListView list={rootStore} />

<br />

<div>{status.connected ? "Online (sync enabled)" : "Offline (sync disabled)"}</div>
<button
type="button"
onClick={() => {
toggleWebrtcProviderConnection()
}}
style={{ width: "fit-content" }}
>
{status.connected ? "Disconnect" : "Connect"}
</button>
</>
)
})

app.tsx

examples/clientServer/app.tsx
import { observer } from "mobx-react"
import { AppInstance } from "./appInstance"

let iframeResizerChildInited = false

function initIframeResizerChild() {
if (!iframeResizerChildInited) {
iframeResizerChildInited = true
void import("@iframe-resizer/child")
}
}

export const App = observer(() => {
initIframeResizerChild()

return (
<>
<div
style={{
// to avoid the iframe end from being cut off
paddingBottom: 8,
}}
>
<h2>App Instance</h2>
<AppInstance />
</div>
</>
)
})