Yes, I was doing something wrong!
When subscribing to the Y.Array with preact’s useSyncExternalStore, I was returning the Y.Array itself as a snapshot.
That’s the error: useSyncExternalStore requires snapshots to be immutable copies of the shared, mutable data.
I asked the same question in Delta Chat’s forum, and WofWca noticed what was wrong and proposed a fix that works: Preact: modifying shared Y.Array doesn’t re-render anything - #2 by WofWca - webxdc - Delta.Chat.
I’ll copy some things here.
WofWca pointed out:
What you are doing inside your getSnapshot function is return the same object every time. Which, I assume, what React detects as “the value didn’t change, so I don’t need to re-render”. Here is a solution that I came up with:
diff --git a/js/ystore.js b/js/ystore.js
index a1e7034..6d3e1f4 100644
--- a/js/ystore.js
+++ b/js/ystore.js
@@ -22,6 +22,7 @@ export const provider = new WebxdcProvider({
class YStore {
#isSubscribed = false;
#listeners = new Set();
+ #cached = uuidsArray.toJSON()
#handleEvent = (event) => {
console.log("Received an event for the YStore!");
@@ -29,6 +30,8 @@ class YStore {
console.log(`There are ${this.#listeners.size} listeners.`);
+ this.#cached = uuidsArray.toJSON()
+
this.#listeners.forEach((l) => {
console.log("Calling a listener.");
@@ -72,7 +75,7 @@ class YStore {
// If I do this, the app freezes: empty page, infinite logs in the web browser console...
// return uuidsArray.toArray();
- return uuidsArray;
+ return this.#cached;
};
}
More parts of the code base to have more context:
“Frontend” code (it hasn’t changed):
import { html } from "./html.js";
import * as yStore from "./ystore.js";
export default function App(props) {
console.log("App");
const uuids = yStore.useUuids();
function addRandomUuid() {
yStore.uuidsArray.push([crypto.randomUUID()]);
}
return html`
<h1>UUID manager</h1>
<button onClick=${() => addRandomUuid()}>Add random UUID</button>
<p>UUIDs:</p>
${uuids.map((uuid) => html`<p>${uuid}</p>`)}
`;
}
Yjs-related logic before applying the fix:
import * as Y from "yjs";
import WebxdcProvider from "y-webxdc";
import { useSyncExternalStore } from "preact/compat";
const ydoc = new Y.Doc();
export const uuidsArray = ydoc.getArray("uuids");
export const provider = new WebxdcProvider({
webxdc,
ydoc,
autosaveInterval: webxdc.sendUpdateInterval,
getEditInfo: () => {
const document = "uuids";
return { document };
},
});
/**
* I copied this code from another WebXDC app that works.
*/
class YStore {
#isSubscribed = false;
#listeners = new Set();
#handleEvent = (event) => {
console.log("Received an event for the YStore!");
console.log(`uuidsArray has ${uuidsArray.length} elements.`);
console.log(`There are ${this.#listeners.size} listeners.`);
this.#listeners.forEach((l) => {
console.log("Calling a listener.");
l();
});
};
subscribe = (listener) => {
console.log("Subscribed!");
this.#listeners.add(listener);
if (!this.#isSubscribed) {
console.log("Observing!");
uuidsArray.observe(this.#handleEvent);
this.#isSubscribed = true;
}
return () => {
console.log("Unsubscribing!");
this.#listeners.delete(listener);
if (this.#listeners.size === 0) {
console.log("Unobserving!");
uuidsArray.unobserve(this.#handleEvent);
this.#isSubscribed = false;
}
};
};
getSnapshot = () => {
// This is an object.
console.log(uuidsArray);
// This is an array.
console.log(uuidsArray.toArray());
// If I do this, the app freezes: empty page, infinite logs in the web browser console...
// return uuidsArray.toArray();
return uuidsArray;
};
}
const uuidsStore = new YStore();
export function useUuids() {
return useSyncExternalStore(uuidsStore.subscribe, uuidsStore.getSnapshot);
}
Yjs-related logic after applying the fix:
import * as Y from "yjs";
import WebxdcProvider from "y-webxdc";
import { useSyncExternalStore } from "preact/compat";
const ydoc = new Y.Doc();
export const uuidsArray = ydoc.getArray("uuids");
export const provider = new WebxdcProvider({
webxdc,
ydoc,
autosaveInterval: webxdc.sendUpdateInterval,
getEditInfo: () => {
const document = "uuids";
return { document };
},
});
/**
* I copied this code from another WebXDC app that works.
*/
class YStore {
#isSubscribed = false;
#listeners = new Set();
/**
* preact's useSyncExternalStore needs immutable data to work correctly.
* We will store immutable snapshots of the Yjs data here.
*/
#immutableSnapshot = uuidsArray.toArray();
#handleEvent = (event) => {
console.log("Received an event for the YStore!");
console.log(`uuidsArray has ${uuidsArray.length} elements.`);
console.log(`There are ${this.#listeners.size} listeners.`);
this.#immutableSnapshot = uuidsArray.toArray();
this.#listeners.forEach((l) => {
console.log("Calling a listener.");
l();
});
};
subscribe = (listener) => {
console.log("Subscribed!");
this.#listeners.add(listener);
if (!this.#isSubscribed) {
console.log("Observing!");
uuidsArray.observe(this.#handleEvent);
this.#isSubscribed = true;
}
return () => {
console.log("Unsubscribing!");
this.#listeners.delete(listener);
if (this.#listeners.size === 0) {
console.log("Unobserving!");
uuidsArray.unobserve(this.#handleEvent);
this.#isSubscribed = false;
}
};
};
getSnapshot = () => {
// Return a snapshot of the immutable data.
// Simply doing this doesn't work (the app freezes: empty page, infinite logs in the web browser console...):
// return uuidsArray.toArray();
return this.#immutableSnapshot;
};
}
const uuidsStore = new YStore();
export function useUuids() {
return useSyncExternalStore(uuidsStore.subscribe, uuidsStore.getSnapshot);
}