This is a wrapper for Yjs that aims to provide a consistent immutable API.
y-immutable combines Yjs, a CRDT library with a mutation-based API, with immer, a library for immutable data manipulation using plain JS objects. It is a fork of sep2/immer-yjs.
For vanilla React, the most efficent way is to use useSyncExternalStoreWithSelector:
import bind from 'y-immutable'
// Setup Y store
const doc = new Y.Doc()
const ymap = doc.getMap('appstate.v1')
// Attach y-immutable
const binder = bind(ymap);
// Initialize state
binder.update((state) => (
// y-immutable data uses the format [ type, value ]
['YMap': {
count: ['number', 0]
}]
));
// Define an Immer helper hook
function useYImmutable<Selection>(selector: (state: State) => Selection) {
const selection = useSyncExternalStoreWithSelector(
binder.subscribe,
binder.get,
binder.get,
selector
)
return [selection, binder.update]
}
// use in component
function Component() {
const [count, update] = useYImmutable((s) => s[1].count[1]) // Don't forget the [1] after each prop!
const handleClick = () => {
update((s) => {
// any operation supported by immer
s[1].count[1]++
})
}
// will only rerender when 'count' changed
return <button onClick={handleClick}>{count}</button>
}
// when done
binder.unbind()
For Zustand, see the recipes doc.
Yjs data is encoded in Immer as [type, value]
tuples. This is done so that Yjs CRDTs can be represented as plain JS objects (POJOs).
import { deserialize } from 'y-immutable'
const json = ['YArray', [
['YMap', {
title: ['string', 'write tests'],
checked: ['boolean', true]
}],
['YMap', {
title: ['string', 'dogfood app'],
checked: ['boolean', false]
}]
]]
const ydata = deserialize(json)
The equivalent Yjs object can be converted back to a POJO:
import { serialize } from 'y-immutable'
const yArray = Y.Array.from
const yMap = data => new Y.Map(Object.entries(data))
const ydata = yArray([
yMap({
title: 'write tests',
checked: true
}),
yMap({
title: 'dogfood app',
checked: false
})
])
const json = serialize(ydata)
Supported types are:
The binder wraps a Yjs doc in an immer wrapper, which can then be subscribed to.
import bind from 'y-immutable'
.- Create a binder:
const binder = bind(doc.getMap("state"))
. - Add subscription to the snapshot:
binder.subscribe(listener)
.- Mutations in
y.js
data types will trigger snapshot subscriptions. - Calling
update(...)
(similar toproduce(...)
inimmer
) will update their correspondingy.js
types and also trigger snapshot subscriptions.
- Mutations in
- Call
binder.get()
to get the latest snapshot. - (Optionally) call
binder.unbind()
to release the observer.