I have a prosemirror based editor that I'd like to enable real-time collaboration on. So I've set up a socket server as described here
I set up the WebsocketProvider
as a useRef()
so that we're not constantly re-creating it everytime we render the component (before I was spinning up dozens of websockets). However, now it's not even getting defined, as the console.log(this.yXmlFragment, this.provider)
in get plugins()
is returning both as undefined
My desired behavior is that I want provider
and yXmlFragment
to be updated only when the sectionID
changes, not for any other re-render. But it's not even being set the first time. Can anyone explain how I'm wrong?
import React, { useContext, useEffect, useRef } from "react";
import Editor, { Extension } from "rich-markdown-editor";
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
import { ySyncPlugin, yCursorPlugin } from 'y-prosemirror';
import { AuthUserContext } from "../Util/AuthUser";
type EditorInput = {
id?: string;
readOnly?: boolean;
defaultValue?: string;
value?: string;
placeholder?: string;
sectionID?: string;
onChange(e: string): void;
};
const EditorContainer: React.FC<EditorInput> = (props) => {
const {
id,
readOnly,
placeholder,
sectionID,
defaultValue,
value,
onChange,
} = props;
const { currentUser } = useContext(AuthUserContext);
const provider = useRef<WebsocketProvider>();
const yXmlFragment = useRef<Y.XmlFragment>();
useEffect(() => {
const ydoc = new Y.Doc();
yXmlFragment.current = ydoc.getXmlFragment('prosemirror');
provider.current = new WebsocketProvider('wss://my_socket_server.herokuapp.com', `${sectionID}`, ydoc);
}, [sectionID]);
return (
<div className="Editor-Container" id={id}>
<Editor
onChange={(e: any) => onChange(e)}
defaultValue={defaultValue}
value={value}
readOnly={readOnly}
placeholder={placeholder}
extensions={[
new yWebsocketExt(provider.current, yXmlFragment.current, currentUser)
]}
/>
</div>
);
};
export default EditorContainer;
class yWebsocketExt extends Extension {
provider?: WebsocketProvider;
yXmlFragment?: Y.XmlFragment;
currentUser: User;
constructor(provider: WebsocketProvider | undefined, yXmlFragment: Y.XmlFragment | undefined, currentUser: User) {
super();
if (provider?.shouldConnect) {
provider?.connect()
} else {
provider?.disconnect()
}
this.provider = provider;
this.yXmlFragment = yXmlFragment;
this.currentUser = currentUser;
}
get name() {
return "y-websocket sync";
}
get plugins() {
console.log(this.yXmlFragment, this.provider);
if (this.yXmlFragment && this.provider) {
this.provider.awareness.setLocalStateField('user', {
name: currentUser.name,
color: '#1be7ff',
});
return [
ySyncPlugin(this.yXmlFragment),
yCursorPlugin(this.provider.awareness),
]
}
return []
}
};
This is happening because you are including
sectionID
in the dependency list of theuseEffect
withinEditorContainer
. BecausesectionID
doesn't change on initial load, thisuseEffect
never fires. I've created a minimal example of this here: https://codesandbox.io/s/focused-babbage-ilx4j?file=/src/App.jsI recommend changing
useEffect
touseMemo
because it runs at least once on initial render ofEditorContainer
. And you still get the performance benefit because it shouldn't rerun unlesssectionID
changes.