Funny enough, I received the same RangeError and lo and behold, googling brought me to this same thread I previously commented on .
Poking around my code, it proved to be quite tedious bug indeed but eventually I realized it was one of the those classic Yjs bugs. So when you load your editor, you must hold off from rendering it until the doc has synced. At least this has been my experience — otherwise you’ll get bugs like these where Yjs can’t transform the empty, default doc (single paragraph) into whatever it receives from the provider.
It’s very confusing but so this is what I do:
const [synced, setSynced] = useState(false)
// Must be ref since in React.StrictMode effects are called twice _before_ useState hooks have chance to update.......
const loading = useRef<Loading | undefined>()
const [initialData, setInitialData] = useState<InitialData | undefined>(undefined)
useEffect(() => {
const cur = loading.current
if (
sessionUser &&
syncToken &&
cur?.docId !== params.docId &&
notEqualUser(cur?.user, sessionUser) &&
cur?.token !== syncToken
) {
initializeEditor(params.docId, sessionUser, syncToken)
}
setCurrentDocId(params.docId)
return () => {
// @TODO this has to be destroyed but since useEffect hooks are so flaky and useless, it has to be done
// by listening to navigation hooks or something, maybe in editor.tsx
// loading.current?.provider.destroy()
setCurrentDocId()
}
}, [params.docId, sessionUser, syncToken])
function initializeEditor(docId: string, user: User & { id: string }, token: string) {
setSynced(false)
loading.current?.provider.destroy()
const ydoc = new Y.Doc()
const provider = new HocuspocusProvider({
url: client.HOCUSPOCUS_URL,
name: docId,
document: ydoc,
token,
onSynced: () => {
setSynced(true)
}
})
loading.current = {
docId,
provider,
token,
user
}
setInitialData({
docId,
ydoc,
provider,
token,
user: {
id: user.id,
name: user.name || 'Unknown',
image: user.image || null
}
})
setSyncToken(token)
}
and in render:
{initialData && synced && <TiptapEditor initialData={initialData} />}
Note the hack for double useEffect
as it’s otherwise quite annoying when developing. But the important part is that the Tiptap editor receives the initial data only when the provider has fully synced. In other version I don’t destroy the editor in-between changes — just keep the old one until the new one has loaded — but it’s a lot easier this way.