@useblok/yjs
v0.2.0
Published
Realtime multi-user sync for Blok over Yjs. Docs: https://docs.useblok.dev
Downloads
87
Maintainers
Readme
@useblok/yjs
Realtime multi-user sync for Blok over Yjs. Wires a BlokStore to a Y.Doc so any y-* provider (WebSocket, WebRTC, IndexedDB, libp2p) can broadcast a document across a room.
Install
npm install @useblok/yjs yjs y-websocketyjs is a peer dependency — you control the version.
Quickstart
"use client";
import * as React from "react";
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
import { Blok, BlokStoreProvider, createBlokStore } from "@useblok/core";
import { useYjsBinding } from "@useblok/yjs";
import { config } from "./blok.config";
function SyncedEditor({ roomId }: { roomId: string }) {
const ydoc = React.useMemo(() => new Y.Doc(), [roomId]);
React.useEffect(() => {
const provider = new WebsocketProvider(
"wss://your-yjs-server.example.com",
roomId,
ydoc,
);
return () => provider.destroy();
}, [ydoc, roomId]);
useYjsBinding(ydoc);
return <Blok config={config} />;
}
export default function Page({ roomId }: { roomId: string }) {
const store = React.useMemo(() => createBlokStore({ config }), []);
return (
<BlokStoreProvider store={store}>
<SyncedEditor roomId={roomId} />
</BlokStoreProvider>
);
}That's it. Two browsers pointed at the same roomId now see each other's changes.
What this does (v0.1)
- Whole-document sync. On every local change, the serialized
Datais written to aY.Mapunder key"data". Remote changes to that key are applied viastore.setData. - Loop-safe. Local writes use a private transaction origin; the remote observer filters by origin to prevent echo loops.
- Provider-agnostic. Bring any Yjs transport:
y-websocket,y-webrtc,y-indexeddb, Hocuspocus, Liveblocks, Partykit. - SSR-tolerant. Call
useYjsBindinginside a"use client"component — Yjs itself runs fine in Node but providers typically assume browser.
Known limitations
- Last-writer-wins on concurrent same-field edits. Two users typing into the same richtext field will not merge character-by-character; the last commit wins. Per-field CRDT structures (Y.Text / Y.Array / Y.Map) are a planned v0.2 upgrade and will not break this API.
- Remote updates land in local history. Because sync routes through
setData, a remote change becomes part of the local undo stack. A "silent set" action will land with the per-field upgrade. - Large documents broadcast in full. Every keystroke writes the whole
DataJSON into Y. Fine for typical marketing pages; not ideal for very large documents. The v0.2 per-field structures fix this too.
API
createYjsBinding(store, doc, options?)
| Param | Type | Notes |
| ------------------------ | --------------- | -------------------------------------- |
| store | BlokStore | From createBlokStore(...). |
| doc | Y.Doc | Your Y.Doc instance. |
| options.mapName | string | Y.Map name. Default "blok". |
| options.dataKey | string | Key inside the map. Default "data". |
| options.onRemoteUpdate | (data) => void| Inspect / log merges from the network. |
| options.onLocalUpdate | (data) => void| Inspect / log outgoing broadcasts. |
Returns { map, destroy() }. Call destroy() to detach observers when you're done.
useYjsBinding(doc, options?)
Thin React wrapper over createYjsBinding. Reads the store from the surrounding <BlokStoreProvider>. Pass { enabled: false } to hold the sync off without unmounting.
createYjsPresence(awareness) / useYjsPresence(awareness)
Wraps a y-protocols/awareness Awareness instance in Blok's PresenceBinding shape. Pass the binding to <Blok presence={...} /> to render live peer cursors and avatars. Your awareness instance comes from whichever provider you use (WebsocketProvider, WebrtcProvider, Hocuspocus, etc.).
const provider = new WebsocketProvider(url, roomId, ydoc);
const presence = useYjsPresence(provider.awareness);
<Blok
config={config}
presence={presence}
presenceIdentity={{ id: user.id, name: user.name }}
/>createYjsComments(doc, options) / useYjsComments(doc, options)
Shared comments backed by a Y.Map. Every create / update / delete propagates to every connected client — same tab or different peer — and subscribers fire live events. Pass the binding to <Blok comments={...} />.
const comments = useYjsComments(ydoc, {
author: { id: user.id, name: user.name },
documentId: pageId, // isolate multi-doc rooms
});
<Blok config={config} comments={comments} commentAuthor={author} />Options:
| Param | Type | Notes |
| -------------- | --------------- | -------------------------------------------------------------------- |
| author | CommentAuthor | Required. Stamped on every comment this client creates. |
| documentId | string | Isolates rooms that share one Y.Doc across docs. Default "default". |
| mapName | string | Y.Map name. Default "blok-comments". |
| newId | () => string | Override the id generator (default: crypto.randomUUID). |
