I want to introduce you to the useY hook (react-yjs package). It’s a new React hook designed to simplify working with Yjs data structures in your React applications.
The useY hook automatically subscribes to changes in the Yjs data-structure and triggers re-renders when the data changes. It also returns the result of the .toJSON method from the Yjs data-structure, making it easy to work with your Yjs data in React components.
For (deeply) nested structures (maps contained in maps and arrays) it is not always optimal to observeDeep and flatten the data using toJSON(), as this will trigger a full update when something in the leaf nodes changes.
I’ve built solutions where individual map keys are observed instead, or in case of lists, only changes to the list itself are observed. This is to minimize the scope of change in the React components. Here’s a CodeSandbox example of the idea. In your browser developer tools you can observe that only the changed components are actually re-rendered.
Yet I’m sure different approaches are suitable to different applications. It would be awesome to have a hook library to support both deep flattening and more “surgical” updates.
I was thinking about it as well, but in my case I had no use-case for the “shallow” use-case and therefor stopped investigating.
Initially I was thinking about a second options param to the useY hook that would accept and option: { deepObserve: false }. By default it’s true. Maybe I’m thinking to complicated but with useSyncExternalStore you need a stable object reference and I wasn’t sure how to achieve that without a deep observe.
From the API design standpoint, I would not use a “deep” parameter but rather expose two separate hooks. The deep one flattens the nested YMaps and Arrays while the shallow one doesn’t. Hence, the type signatures for return values would be quite different, assuming an improved typing is introduced. In my Sandbox example, I’ve also introduced alternative TypedYDoc and TypedYMap typings to allow specifying the exact structure of the whole document.
Unfortunately, useSyncExternalStore, tearing and other async rendering problems of React 18 are not familiar to me, so cannot help with that. I’ll probably just disable concurrent rendering as I’m not looking forward to more non-determinism in my applications. So far I seem to be just fine with useState and useEffect as seen in the hooks I linked above.
Got it, hmm not sure it’s a good addition for the library then. If we introduce an API that requires to turn off concurrent mode. After all it’s the new default.
Now wondering if there should be a section in the README.md hinting on how to create such a hook if you really need it. Or maybe you want to create a separate library I can link to?
btw the const [id] = useYField(item, "id"); could be done as const id = useY(item.get("id")).
Hey @nikgraf, thank you for publishing this! I built something similar for Y.js awareness in the past. It also uses useSyncExternalStore to synchronize with the external awareness store.
Small note: If you want to support older React versions (with support for hooks), you may want to use the the backwards compatible shim for useSyncExternalStore available as a npm package here.
This is great! Thought about building something similar, but didn’t investigate yet. I was thinking about useAwareness for react-yjs to stick to the yjs API style.
Yours though looks more approachable. Great job! Are you happy with the API?
Good catch @ useSyncExternalStore. Thanks! To keep things lightweight I decided to not support older React versions. That problem should solve itself over time when more projects get upgraded