Should we do state management directly with yjs, or through bindings like SyncedStore?

I recently read an opinion that it is better to do state management directly with yjs than through some state management library.

CRDT is not (only) a data structure for multi-person collaboration, it already covers the front-end “state management” capabilities itself, including mechanisms such as transaction and batch. From this perspective, there seems to be no clear need for developers of collaborative applications to add another layer of “state management libraries” on top of it, unless it is for compatibility with older projects

Should we do state management directly with yjs, or through bindings like SyncedStore?

What do you think?

It really depends. There are arguably nicer libraries for state management. Yjs is meant to have a minimalistic interface for observing changes (and calculating differences) that can be integrated into other libraries. It doesn’t have the reactive interface of mobx, and isn’t immutable like Redux. There is no big performance loss in integrating Yjs into an existing library. It might be worth it.

SyncedStore is awesome, use it if you like it.

However, nothing is perfect. Most users of state management libraries don’t think too much about properly organizing data for managing conflicts, optimizing shared data for performance, and migrating existing data to new schemas. React data stores are, in my opinion, too close to the representation. You likely want to reorganize data when the structure of your application changes. synced-store might make it too easy to make an existing application collaborative. Instead, you should think very carefully about the schema of your data. Because it’s really hard to change it once you have active users.

4 Likes

Is there an example of using yjs without synced store in React? I use PureScript and the SyncedStore API really doesn’t lend itself well to an immutable language.

Here’s the hook I use for Y.Map. Other shared types would be very similar, and the argument type is currently set up to handle any Yjs shared type. It performs a shallow comparison between new and old state to determine re-renders. The generic type T refers to the type of the map value.

const useYMap = <T>(yobj: Y.AbstractType<T>): Record<string, ExtractYEvent<T>> => {
  const [state, setState] = useState<Record<string, ExtractYEvent<T>>>(yobj.toJSON())

  const updateState = useCallback(async e => {
    const stateNew: Record<string, ExtractYEvent<T>> = yobj.toJSON()
    setState((stateOld: Record<string, ExtractYEvent<T>>) => 
      !shallowEqual(stateNew, stateOld) ? stateNew : stateOld
    )
  }, [])

  useEffect(() => {
    yobj.observe(updateState)
    return () => {
      yobj.unobserve(updateState)
    }
  }, [])

  return state
}

Usage:

const myvar = useYMap(doc.getMap<MyType>())

The ExtractYEvent type I am using is:

// Infer the generic type of a specific YEvent such as YMapEvent or YArrayEvent
// This is needed because YEvent is not generic.
type ExtractYEvent<T> = T extends Y.YMapEvent<infer U> | Y.YArrayEvent<infer U> ? U : never

Not sure if the above is much better, as it’s still pretty imperative, but at least it’s plain Yjs and React.

Jelly :eyes:

2 Likes