Combine immer & y.js

Related tags

React immer-yjs
Overview

immer-yjs

npm size

Combine immer & y.js

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

  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.

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

Contributions are welcome

Please open an issue to discuss first if the PR contains significant changes.

Similar projects

valtio-yjs

Comments
  • 'Error: not implemented' when using array `.splice()` to remove items (but not add any)

    'Error: not implemented' when using array `.splice()` to remove items (but not add any)

    Repro:

    Replace this line https://github.com/sep2/immer-yjs/blob/6b9eb377f0a8cf7fd8d2df7286ce5ed4b9a99b48/packages/immer-yjs/src/immer-yjs.test.ts#L52 with

    d1.topping.splice(2, 2)
    

    and then rerun the unit test

    opened by senseibaka 3
  • Generic type `YEvent<T>` requires 1 type argument(s)

    Generic type `YEvent` requires 1 type argument(s)

    We have just imported immer-yjs into our project (and has been great so far!), but it is causing our build to fail. (Even with skipModuleCheck: true set in tsconfig, which is odd and we need to figure out)

    The issue stems from the YEvent type needing a type argument.

    ../../node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/immer-yjs/src/immer-yjs.ts:11:59 - error TS2314: Generic type 'YEvent<T>' requires 1 type argument(s).
    
    11 function applyYEvent<T extends JSONValue>(base: T, event: Y.YEvent) {
                                                                 ~~~~~~~~
    
    ../../node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/immer-yjs/src/immer-yjs.ts:49:64 - error TS2314: Generic type 'YEvent<T>' requires 1 type argument(s).
    
    49 function applyYEvents<S extends Snapshot>(snapshot: S, events: Y.YEvent[]) {
                                                                      ~~~~~~~~
    
    ../../node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/immer-yjs/src/immer-yjs.ts:52:45 - error TS7006: Parameter 'obj' implicitly has an 'any' type.
    
    52             const base = event.path.reduce((obj, step) => {
                                                   ~~~
    
    ../../node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/immer-yjs/src/immer-yjs.ts:52:50 - error TS7006: Parameter 'step' implicitly has an 'any' type.
    
    52             const base = event.path.reduce((obj, step) => {
                                                        ~~~~
    
    ../../node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/immer-yjs/src/immer-yjs.ts:195:31 - error TS2314: Generic type 'YEvent<T>' requires 1 type argument(s).
    
    195     const observer = (events: Y.YEvent[]) => {
                                      ~~~~~~~~
    
    Found 5 errors in the same file, starting at: ../../node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/immer-yjs/src/immer-yjs.ts:11
    

    Adding a generic type argument of any fixes the the type issue. If there is a better type argument, I would be happy to update the PR with it!

    opened by kculmback-eig 2
  • XML and Text Support

    XML and Text Support

    In my use case I only need to pass through the XML Fragments to different rich text editors, not mutate or read the rendered contents directly. I have many, and they are nested.

    Even after customizing applyPatch to pass-trhough the XML and text YJS objects, immer-yjs calls toJSON on the doc instance, which makes the XMLFragment transform into a string.

    Furthermore, this string never gets updated because the text events are never listened to.

    The other suggested option is keeping the XML fragment objects outside immer. In this case, do you have any suggestions to make these reactive?

    SyncedStore does implement all of this, but the mutable/FRP API is not for everyone.

    opened by luishdz1010 2
  • More tests needed

    More tests needed

    The library needs more tests on mutation by calling update() and by modifying y.js types. After the mutation, we should expect their opposite is updated as well. Each modification operation should be independently tested.

    Helps are welcome.

    opened by sep2 0
  • Benchmarks and comparison to SyncedStore discussion

    Benchmarks and comparison to SyncedStore discussion

    Hey @sep2, Thanks for making immer-yjs!

    I have been thinking about making something similar and I am glad someone is already doing that :) (in my ideal world, one could even swap yjs with fluid framework or whatever, but thats for another time)

    I have been playing with SyncedStore and I wonder how the two solutions compare in developer experience and performance. Some of my initial thoughts:

    Developer Experience Advantages

    1. immer-yjs has a plain object snapshot and translates immer patches to yjs, while syncedstore uses yjs as the store, and creates proxies around them. This means that reading the store is easy, no annoying proxies and cloning issues.

    2. immer-yjs is also opinionated in the sense that an array set calls the yjs splice method by default - which might be not ideal in a collaborative setting. See my discussion here for reference. In any case, that is something that could potentially be configured in the future.

    3. Wrapping everything around a produce plays nicely with transactions, which could be useful with the yjs support for transaction based undo/redo.

    Sharing Granularity

    The main issue I currently see with the "magic solution" is the crdt granularity. In most cases, you don't want the whole nested object to be composed of nested crdts, as this might create undesirable states. This is solved in SyncedStore with the Boxed values. This works but kind of annoying - you need to constantly wrap objects with a Box and then call .value on everything.

    The ideal solution, in my opinion, is defining a schema and then updating the crdt's based on the schema. Are you open to a PR that adds support for something like that?

    Performance

    How does immer patches and maintaining the snapshot compare to the proxy based solution of synced store? I wonder what would be a good test for that.

    Would love to hear your opinion about this, and if you are accepting PR's

    opened by bogobogo 9
Owner
Felix
Felix
A helper to use immer as Solid.js Signal to drive state

Solid Immer A helper to use immer as Solid.js Signal to drive state. Installation $ npm install solid-immer Usage Use createImmerSignal to create a im

Shuaiqi Wang 67 Nov 22, 2022
A tiny package for JavaScript Web App's state management based on RxJS & Immer

A tiny package for JavaScript Web App's state management based on RxJS & Immer

Xiao Junjiang 12 Oct 19, 2022
Combine type and value imports using Typescript 4.5 type modifier syntax

type-import-codemod Combines your type and value imports together into a single statement, using Typescript 4.5's type modifier syntax. Before: import

Ian VanSchooten 4 Sep 29, 2022
We combine intensive technology with design expertise.

basement laboratory basement lab is carving out a vanguard position by combining intensive technology with formal design expertise – we are making the

basement.studio 47 Dec 30, 2022
📦 Fully typed and immutable store made on top of Immer with mutation, action, subscription and validation!

Riux is a fully typed and immutable store made on top of Immer with mutation, action, subscription and validation! Table of contents ?? Installation U

null 10 Aug 27, 2022