Combine immer & y.js
immer is a library for easy immutable data manipulation using plain json structure. y.js is a CRDT library with mutation-based API. immer-yjs
allows manipulating y.js
data types with the api provided by immer
.
- Two-way binding between y.js and plain (nested) json object/array.
- Efficient snapshot update with structural sharing, same as
immer
. - Updates to
y.js
are explicitly batched in transaction, you control the transaction boundary. - Always opt-in, non-intrusive by nature (the snapshot is just a plain object after all).
- The snapshot shape & y.js binding aims to be fully customizable.
- Typescript all the way (pure js is also supported).
- Code is simple and small, no magic hidden behind, no vendor-locking.
Do:
// any operation supported by immer
update(state => {
state.nested[0].key = {
id: 123,
p1: "a",
p2: ["a", "b", "c"],
}
})
Instead of:
Y.transact(state.doc, () => {
const val = new Y.Map()
val.set("id", 123)
val.set("p1", "a")
const arr = new Y.Array()
arr.push(["a", "b", "c"])
val.set("p2", arr)
state.get("nested").get(0).set("key", val)
})
yarn add immer-yjs immer yjs
import { bind } from 'immer-yjs'
.- 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.
Y.Map
binds to plain object {}
, Y.Array
binds to plain array []
, and any level of nested Y.Map
/Y.Array
binds to nested plain json object/array respectively.
Y.XmlElement
& Y.Text
have no equivalent to json data types, so they are not supported by default. If you want to use them, please use the y.js
top-level type (e.g. doc.getText("xxx")
) directly, or see Customize binding & schema section below.
🚀🚀🚀 Please see the test for detailed usage. 🚀🚀🚀
Use the applyPatch
option to customize it. Check the discussion for detailed background.
By leveraging useSyncExternalStoreWithSelector.
import { bind } from 'immer-yjs'
// define state shape (not necessarily in js)
interface State {
// any nested plain json data type
nested: { count: number }[]
}
const doc = new Y.Doc()
// optionally set initial data to doc.getMap('data')
// define store
const binder = bind<State>(doc.getMap('data'))
// define a helper hook
function useImmerYjs<Selection>(selector: (state: State) => Selection) {
const selection = useSyncExternalStoreWithSelector(
binder.subscribe,
binder.get,
binder.get,
selector,
)
return [selection, binder.update]
}
// optionally set initial data
binder.update(state => {
state.nested = [{count: 0}]
})
// use in component
function Component() {
const [count, update] = useImmerYjs((s) => s.nested[0].count)
const handleClick = () => {
update(s => {
// any operation supported by immer
s.nested[0].count++
})
}
// will only rerender when 'count' changed
return <button onClick={handleClick}>{count}</button>
}
// when done
binder.unbind()
Please submit with sample code by PR, helps needed.
Data will sync between multiple browser tabs automatically.
Please open an issue to discuss first if the PR contains significant changes.