Awesome
immer-yjs
Combine immer & y.js
Maintainer Wanted
The function of this package is stable. Since the ecosystem around changes very ofen and I no longer work with Y.JS so can't keep pace with it, the package will like to have new maintainers. If you would like to maintain this package please submit a PR to remove this notice.
What is this
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)
})
Installation
yarn add immer-yjs immer yjs
Documentation
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.
With Vanilla Javascript/Typescript
🚀🚀🚀 Please see the test for detailed usage. 🚀🚀🚀
Customize binding & schema
Use the applyPatch
option to customize it. Check the discussion for detailed background. This section will likely be removed since it is not functioning properly. A new impl may be needed
Integration with React
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()
Integration with other frameworks
Please submit with sample code by PR, helps needed.
Demos
Data will sync between multiple browser tabs automatically.