redux-yjs-bindings
v0.3.1
Published
Use Yjs to sync your Redux store with other peers!
Downloads
83
Maintainers
Readme
redux-yjs-bindings
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 includesnpm 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".