How to use Yjs subdocuments

Liveblocks Yjs supports subdocuments, which allow you to nest Yjs documents inside each other. This guide takes you through how to use them on client and server.

When to use subdocuments

Subdocuments are helpful when you have multiple large Yjs documents in the same room, and you wish to lazy-load them individually. Each subdocument works similarly to a normal Yjs document, allowing you to use shared types, awareness, and more.

Not necessary for multiple text editors

Please note that subdocuments are not necessary for displaying multiple text editors on one page. For this use case, it’s often best to create a Y.Map in your Yjs document, and place the contents of each editor inside. For example, if your text editor uses Y.XmlFragment, here’s how to create this.

// Create Yjs document with an `editors` mapconst yDoc = new Y.Doc();const yMap = yDoc.getMap("editors");
// Create shared types and add to mapconst editorOne = new Y.XMLFragment();const editorTwo = new Y.XMLFragment();yMap.set("editor-1", editorOne);yMap.set("editor-2", editorTwo);
// Pass `editorOne` and `editorTwo` to your text editors// ...

This is much simpler than using subdocuments. However, if the content of these editors is very large, or if your text editor only accepts a Y.Doc, subdocuments may be for you.

On the client

Subdocuments can be stored in your Yjs tree like any other shared type. In this example we’ll create a Y.Map to store them in, making sure to lazy load any subdocuments.

import { LiveblocksProvider } from "@liveblocks/yjs";import * as Y from "yjs";
// Create main document and connect, disabling auto-loading of subdocumentsconst yDoc = new Y.Doc();const yProvider = new LiveblocksProvider(room, doc, { autoloadSubdocs: false });
// Create a Y.Map to hold subdocumentsconst subdocMap = yDoc.getMap("subdocs");

Create a subdocument

To create a new subdocument, create a new Y.Doc(), and use this it any other.

import { LiveblocksProvider } from "@liveblocks/yjs";import * as Y from "yjs";
// Create main document and connect, disabling auto-loading of subdocumentsconst yDoc = new Y.Doc();const yProvider = new LiveblocksProvider(room, doc, { autoloadSubdocs: false });
// Create a Y.Map to hold subdocumentsconst subdocMap = yDoc.getMap("subdocs");
// Create subdocumentconst subdoc = new Y.Doc();yDoc.getMap().set("my-document", subdoc);subdoc.getText("default").insert(0, "This is a subdocument");
// Make note of its `guid`, which is used for retrieving it laterconst guid = subdoc.guid; // e.g. "c4a755..."

Make sure to keep track of its guid.

Load the subdocument

To load the subdocument on another client, use its guid.

import { LiveblocksProvider } from "@liveblocks/yjs";import * as Y from "yjs";
// Create main document and connect, disabling auto-loading of subdocumentsconst yDoc = new Y.Doc();const yProvider = new LiveblocksProvider(room, doc, { autoloadSubdocs: false });
// From another client, load the subdoc using the GUID from `subdoc.guid` or `doc.getSubdocGuids`yProvider.loadSubdoc("c4a755...");
// Alternatively, get a reference to a subdocument from `doc.getSubdocs()` and then load// subdoc.load();

Listening for changes

To keep track of subdocument changes, you can use Y.Doc.on("subdocs").

import { LiveblocksProvider } from "@liveblocks/yjs";import * as Y from "yjs";
// Create main document and connect, disabling auto-loading of subdocumentsconst yDoc = new Y.Doc();const yProvider = new LiveblocksProvider(room, yDoc, { autoloadSubdocs: false,});
yDoc.on("subdocs", ({ added, removed, loaded }) => { // Subdocument change // ...});

On the server

It’s possible to use your subdocument on the server, without connecting with a provider.

Fetching a subdocument

When fetching a Yjs subdocument on the server, it’s recommended to use liveblocks.getYjsDocumentAsBinaryUpdate with the guid of your subdocument. We stored the guid when we created the subdocument.

import { Liveblocks } from "@liveblocks/node";import * as Y from "yjs";
const liveblocks = new Liveblocks({ secret: "",});
export function POST() { // Get your Yjs subdocument as a binary update const update = liveblocks.getYjsDocumentAsBinaryUpdate("my-room-id", { // The `guid` of your subdocument, as noted earlier guid: "c4a755...", });
// Create a Yjs document for your subdoc and apply the update const subdoc = new Y.Doc(); Y.applyUpdate(subdoc, new Uint8Array(update));
// `subdoc` can now be read // ...}

We’ve now retrieved the subdocument, and it can be read, but any changes you make won’t be applied to other clients, and are only temporary.

Updating a subdocument

To permanently apply changes to your subdocument, sending them to Liveblocks and other clients, you can use liveblocks.sendYjsBinaryUpdate.

import { Liveblocks } from "@liveblocks/node";import * as Y from "yjs";
const liveblocks = new Liveblocks({ secret: "",});
export function POST() { // Get your Yjs subdocument as a binary update const update = liveblocks.getYjsDocumentAsBinaryUpdate("my-room-id", { // The `guid` of your subdocument, as noted earlier guid: "c4a755...", });
// Create a Yjs document for your subdoc and apply the update const subdoc = new Y.Doc(); Y.applyUpdate(subdoc, new Uint8Array(update));
// Make changes to your `subdoc`, for example subdoc.getText("my-text").insert(0, "Hello world");
// Convert `subdoc` into a binary update const subdocChanges = Y.encodeStateAsUpdate(subdoc);
// Send the changes to Liveblocks, and other clients liveblocks.sendYjsBinaryUpdate("my-room-id", subdocChanges, { // The `guid` of your subdocument, as noted earlier guid: "c4a755...", });}

After running this code, all connected users will see the update.