Reactive CRDT: easy to use API to use Yjs and build collaborative apps

Hi all! A couple of weeks ago I’m got started with Yjs. I loved the project and its potential to develop local-first, distributed applications.

However, I noticed a bit of a steep learning curve at the beginning (“How do all these shared types work? How do I compose them?”). After getting the gist of it, wanted to see if I can build a really easy library to use Yjs.

The first version of Reactive CRDT is now live at https://github.com/yousefED/reactive-crdt. I really appreciate any feedback!

(Have a look at the TODO MVC example and the examples in the readme)

It works particularly easy with React, but it works great standalone as well.

TLDR;

  • store.property.value = 5 instead of doc.getMap("store").getMap("property").set("value", 5)
  • Reactive model that automatically observes changes: autorun(() => { console.log(store.property.value)}) instead of something like doc.getMap("store").observeDeep((event) => console.log(doc.getMap("store").getMap("property").get("value"));

Hope you’ll try it out!

4 Likes

A couple of notes, or things to think about:

  • Some of the imports say import "@reactivedata/reactive"; and others import "@reactivedata/reactive-crdt";
  • Your API makes it extremely simple to map JSON to Yjs types (Y.Map & Y.Array). On the other hand, Y.Xml & Y.Text can’t be mapped to JSON, so maybe it would make sense to export them directly instead? Then it would be easier to create editor bindings within react / vue applications.

I love the whole concept of reactive. I didn’t know that reactive allows to manipulate JSON directly. This is seriously awesome. Did you already publish a package? I want to create a new section in docs.yjs.dev for these kinds of projects. Maybe it would even make sense to link to this in the tutorial section.

Thanks for creating it! :heart:

1 Like

You’re welcome! Would be great to be featured!

  • I’ve just added support for Y xml and text, just use object.property = new Y.Text(). Package has been published! (unfortunately it still uses a fork of Yjs untill we decide about https://github.com/yjs/yjs/pull/298)
  • it’s also possible to pass boxed values with store.property = boxed({prop: val}). Boxed values will not be converted to a Y.Map / Y.Array, but stored as plain JSON (similar to ymap.set("property", {prop: val}) in Yjs

Regarding “reactive” vs. “reactive-crdt”. “reactive” is a separate library I wrote (mostly as a basis for “reactive-crdt”). It’s similar to MobX, NX Observe, or Vue3’s “reactive” function. It brings Functional Reactive Programming (FRP) to Javascript: simply put; the concept of automatic observable values and observers / reactions, instead of having to deal with events all over the place.

reactive-crdt” is the easy-to-use API for Yjs. It can be used with either “reactive”, MobX, or Vue3 reactive (technically, it could also be used without any FRP library, but it would make it difficult to observe changes). The demo in the readme uses the “reactive” package as it has the simplest API. Pretty much all “reactive-crdt” is exposed in just the crdt function!

Coincidentally I’ve been working on a similar thing as a wrapper for the @vue/reactivity package Basic Reactivity APIs | Vue.js (vuejs.org). (basically it proxies a vue reactive object, and sets up data syncing between a vue reactive object and corresponding nested shared types)

The issue I’ve recently run into though is around initialising state offline. It works fine at the doc level, if the associated shared type is at the doc level, i.e. doc.getArray().

However an array that is nested in a map, each client will initially create its own new Y.Array() and insert it into the map, and essentially one of these arrays will be discarded on sync.

Or another example I’ve found is having a primitive value in the json, e.g. a number 0, if the document gets updated to have that field as a 1, when a new client connects it can reset that value to the initial value of 0.

Did you figure out a way around this? or am I overthinking a non-problem (e.g. is it really that hard to ensure some sort of ‘sync’ event before initialising)?

I’ve actually added Vue support to https://github.com/yousefED/reactive-crdt today. Would love your feedback!

Regarding the initial values: that’s not solved in this library. I think it’s better to discuss that in this thread. The topic has come up quite a bit lately, so it would be good to collect feedback and thoughts there.

Looking at the api around the vue support I do have a few preferences/suggestions, which are coincidentally how our apis differ…

That example of using the data field in define component does a great job at highlighting how simple it can be to use for people familiar with vue so A+ on that.

I went a quite different route with my api since in vue3 the reactivity logic is actually separated out from the component framework completely, and I want to use this in environments not supported by the vue framework such as worker threads.

its along the lines of

const fruits = reactive([{ name: 'Apple', weight: 2 }]) // use the vue 'reactive' api
const syncedFruits = vuey(fruits, () => {
  const lists = doc.getMap('lists')
  if (!lists.has('fruits')) lists.set('fruits', new Y.Array())
  return lists.get('fruits')
})

Then you can use syncedFruits as a regular vue reactive object where needed.

The parameters are:

  1. The initial data in a vue reactive object - it doesn’t work well with an offline first strategy, but I have a somewhat successful, if hacky, method of ensuring initialising once (provided every client is synced first).
  2. A sort of Y.Doc mount point getter. This allows the client to check if a nested map or array already exists before creating a new one.

I don’t really think this is the neatest api, but it provides me with a lot of flexibility. As I said I can use this in worker threads, and as
dmonad asked earlier, for instance, if I want to have an xmlFragment in the object it can be this:

vuey(reactive({ counter: 0, text: markRaw(new Y.XmlFragment()) }, () => doc.getMap())

here markRaw and reactive are both from vue’s own reactivity library. markRaw basically tells vue reactive to not react to internal changes of the xmlFragment.

Thanks for the feedback @alexbakerdev!

reactive-crdt should integrate nicely with vue’s “reactive” (it’s also what the data-example uses under the hood) and accommodate your use-case. Let me know (maybe on github) if I can make this clearer in the docs.

The reactive-crdt version of your first example would be:

// setup
import * as Vue from "vue";
import { crdt, useVueBindings } from "@reactivedata/reactive-crdt";
useVueBindings(Vue);

const lists = Vue.reactive(crdt({ fruits: [{ name: 'Apple', weight: 2 }]}));
if (!lists.fruits) lists.fruits = [];

Now you can do with lists.fruits whatever you want. Nested values are also automatically transformed, so { name: 'Apple', weight: 2 } is a YMap internally.

Your second example would be:

Vue.reactive(crdt({ counter: 0, text: new Y.XmlFragment() }));

With reactive-crdt, your application should actually respond to text / xml changes, so you don’t have to call .observe manually.

Hey @YousefED thanks a lot for creating this library, i was wondering what is the solution for Vue2, for now I’m using the without any framework option.

I would greatly appreciate any guidance you could provide.

Hi and welcome to the forums!

Unfortunately, there are no plans to support Vue 2. In Vue 3, the “reactivity” system of Vue has been rewritten, and this is what reactive-crdt relies on. I have not explored Vue 2 enough in-depth to estimate the work required for an integration.

Of course, you can use the “plain” reactive-CRDT library as is :slight_smile: How has your experience been so far?

Can you try the combination of rxjs and YJS, I think