React useRef() not getting defined

561 views Asked by At

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 []
  }
};
1

There are 1 answers

0
Chase Farmer On BEST ANSWER

This is happening because you are including sectionID in the dependency list of the useEffect within EditorContainer. Because sectionID doesn't change on initial load, this useEffect never fires. I've created a minimal example of this here: https://codesandbox.io/s/focused-babbage-ilx4j?file=/src/App.js

I recommend changing useEffect to useMemo because it runs at least once on initial render of EditorContainer. And you still get the performance benefit because it shouldn't rerun unless sectionID changes.