Skip to content

lscheibel/redux-yjs-bindings

Repository files navigation

redux-yjs-bindings

npm package Downloads Issues

Use Yjs to sync your Redux store with other peers!

This very small (roughly 1kB) bridge to connect Redux with a Yjs, allows you to use the synchronization features of Yjs with the data management capabilities of Redux. It works with any Redux store, whether you use Redux Toolkit or not, and even supports initial values. Values that are transmitted over the network can be any JSON serializable value, from primitives to deeply nested object structures.

Install

npm i redux-yjs-bindings

Prerequisites

To use this library you will need a Yjs document as well as a Redux store. Detailed steps on how to set both up can be taken form their respective documentation. You will need to configure the reducer of the state slice that you want to have synced to accept an action dispatched by this library, however helper functions are exported to hide this away. Check below for example usage:

General Usage

import { bind, enhanceReducer } from 'redux-yjs-bindings';
import { Doc } from 'yjs';
import { createStore } from 'redux';
import { mySharedStateReducer } from './store/reducers';

// Create Yjs document, or use existing one.
const yDoc = new Doc();

const rootReducer = {
  // Set up reducer to handle action dispatched by `redux-yjs-bindings` when remote changes come in.
  mySharedState: enhanceReducer(mySharedStateReducer),
};
const store = createStore(rootReducer);

// Start synchronisation of Redux and Yjs
bind(yDoc, store, 'mySharedState');

Internally the bind function creates a new Y.Map() on the provided document. This map then contains a property with the provided sliceName, whose value should then always match the one of your store slice.

Limitations

Despite best efforts to make this library as compatible with existing projects as possible, there are some things to keep in mind.

Referential Equality

For now referential equality is not kept when changes from a remote peer are applied to the local Redux state. This may change in the future, however for now you can simply provide a custom equality function to the useSelector hook if performance becomes a problem.

Undefined in Array ([undefined]) Not Supported

As per Redux guidelines values in the store should be JSON serializable. While undefined values are mostly supported by this library, Yjs currently doesn't support undefined in arrays. However, undefined as a primitive or as object values works just fine, even though they are technically not a part of JSON.

Setter Action Must Be Synchronous

When remote changes come in through Yjs changes must be applied to the store immediately. Since reducers are already required to be synchronous by Redux and the action is dispatched by the library itself, this should not be a problem in practice. However, in theory it would be possible for a Redux middleware to delay all dispatched actions, which could potentially lead to data loss.

Intention Preservation on Array Changes

Internally redux-yjs-bindings subscribes to changes on the Redux store in order to patch the Yjs document. By calculating the difference between the previous and the next state, intentions behind changes can be retrieved even though Redux forces immutability. Unfortunately diffing arrays is not trivial and the currently used algorithm "recursive-diff" by @cosmicanant does not preserve intention for insertions or deletions that are not at the end of the array. States will still be synced correctly, but sometimes with more work than necessary. See this issue for more details.

Holey Arrays ([1, , 'a']) Not Supported

Well this obscure "feature" of JavaScript is simply not supported and will fail almost immediately.

API


bind(yDoc: Y.Doc, store: Store, sliceName: string): unbind()

yDoc

The yjs document changes should be synced with.

store

The redux store changes should be synced with.

sliceName

The name of the state slice (subtree) that should be synced.

unbind()

Returns a function which unbinds the state sync between the YJS doc and redux store


enhanceReducer(currentReducer: Reducer): Reducer

currentReducer

The reducer for the slice of the store that is supposed to be synced.


Constants

ROOT_MAP_NAME Property name of the map on the provided yDoc. Can be used to persist the yjs values on a server.

SET_STATE_FROM_YJS_ACTION Used by enhanceYjsReducer. Action type that is dispatched whenever changes to yjs from other peers come in.


Development

Setup

  • clone repo git clone https://github.com/lscheibel/redux-yjs-bindings
  • install dependencies and start microbundle in watch mode npm i && npm dev
  • open new terminal and navigate to example you want to test on (e.g., cd examples/todo-mvc)
  • follow instructions on how to start example from README.md but usually includes npm i && npm start plus starting the yjs-webrtc signaling server.

Inner Workings

Internally this library simply subscribes to changes on both the Redux store and the Yjs document, while making sure not to react to notifications triggered by itself. The subscribe method on the Redux store can be used to keep track of a previous and current state, from which a difference is calculated (currently using "recursive-diff" by @cosmicanant). The returned list of patch operations is then iterated over to apply all changes to the Yjs document. In the other direction, incoming changes from remote peers through Yjs are handled synchronously in order to prevent local Redux state updates while currently handling remote changes. When remote changes are detected, the Yjs state instance is simply turned into a JavaScript object, which replaces the entire store slice. This comes with the upside, that Yjs is always treated as the source of truth, however referential equality is lost, potentially impacting performance by effectively circumventing Redux selector's caching system, that would usually prevent unnecessary rerenders.

Acknowledgements and Prior Art

Special thanks to Joseph Miles whose zustand-middleware-yjs library was an inspiration. Also look out for Sana Labs upcoming "collaboration-kit".