Home

Awesome

immer-yjs

npm size

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.

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

  1. import { bind } from 'immer-yjs'.
  2. Create a binder: const binder = bind(doc.getMap("state")).
  3. Add subscription to the snapshot: binder.subscribe(listener).
    1. Mutations in y.js data types will trigger snapshot subscriptions.
    2. Calling update(...) (similar to produce(...) in immer) will update their corresponding y.js types and also trigger snapshot subscriptions.
  4. Call binder.get() to get the latest snapshot.
  5. (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.

Changelog

Changelog

Similar projects

valtio-yjs