DraftJs

Enable real-time co-editing for Draft.JS

The guide for adding multiplayer support in applications using React.js and the Draftjs editor.

Basic Integration

Start by adding codox-provider to your DraftJS project:

npm install @codoxhq/codox-provider

The following code snippet can be copy-and-pasted into your project to create a basic integration between a Draft editor component and codox.

import React, { useRef, useEffect, useState } from "react";

// draftjs components
import { Editor, EditorState, convertFromRaw, convertToRaw } from "draft-js";

// import codox-provider
import { withCodox } from "codox-provider";

// decorate editor
const EditorWithCodox = withCodox(Editor, { convertFromRaw, convertToRaw });

// editor container
const EditorContainer = () => {
    // ref for codox api 
  const codoxProviderRef = useRef();
  
  // flag to indicate codox started
  const [codoxStarted, setCodoxStarted] = useState(false);
  
  // local draft editor state
  const [localEditorState, setEditorState] = useState(EditorState.createEmpty());
  
  //Codox config
  const codoxConfig = {
    docId: "[document identifier]",
    username: "user name",
    apiKey: process.env.REACT_APP_CODOX_API_KEY, // codox api key
    autoStart: true,
  };
  
  const setStartCodox = () => setCodoxStarted(true);
  
  // example useEffect to fetch document from some endpoint. 
  // codox should start after the latest document has been 
  // retrieved from the backend.
  useEffect(() => {
    fetch(“[endpoint url]”).then(({ state }) => {
    if (!codoxStarted) {
       // pass fetched state to codox start -
       // codox will initialize update the localEditorState
       // via setEditorState 
       codoxProviderRef.current.start(state);
       setStartCodox();
     }
  });
  
  // will be called on unmount
    return () => {
      // stop codox when the component is unmounted
      codoxProviderRef.current.stop();
    };
  }, []);
  
  // on editor change
  const onEditorChange = (newEditorState) => {
    // all custom state manipulation happens here first  
    
    // … then delegate merging and updating editor to codox
    codoxProviderRef &&
    codoxProviderRef.current &&
    codoxProviderRef.current.onEditorChange(newEditorState);
   }
  };


// render
 return (
   <EditorWithCodox
      // required props for codox
      ref={codoxProviderRef}
      config={codoxConfig}
      editorState={localEditorState}
      setEditorState={setEditorState}
      // draft editor native props
      onChange={onEditorChange}
      // other draftjs options...
    />
  );
};

export default EditorContainer;

Codox HOC

Codox provides a high-order component withCodox(DraftEditor) to wrap the Draft editor and furbish it with multiplayer capabilities. The HOC will perform merging and synchronization tasks automatically as editor states are mutated and when external changes are received. The only imperative interaction it has with the outside world is when to start and stop codox. The HOC expects convertFromRaw and convertToRaw utilities from the 'draft-js'module passed in as helper functions.

import { withCodox } from "codox-provider";

import { Editor, convertFromRaw, convertToRaw } from "draft-js";

const [editorState, setEditorState] = useState(EditorState.createEmpty());

// wrap Editor with codox. 
// should pass convertFromRaw/convertToRaw helpers as options
const EditorWithCodox = withCodox(Editor, { convertFromRaw, convertToRaw });

return (
   <EditorWithCodox
      // required props for codox
      config={codoxConfig}
      editorState={editorState}
      setEditorState={setEditorState}
      onChange={onEditorChange}
    />
  );

The wrapped component expects a codoxConfig object, the editorState and the corresponding setEditorState function, and the onChange callback as props. The editorState the onChange handler will be passed through to the underlying editor component directly. The setEditorState is used by codox to update states after synchronization.

onEditorChange

The editor container would normally pass the setEditorState function directly as the onChange handler, which would allow Draft to directly update the local state. For instance, like so:


const [editorState, setEditorState] = useState(() =>
    EditorState.createEmpty(),
);

return (
    <Editor 
        editorState={editorState} 
        onChange={setEditorState} 
    />;
);

Or, if it chooses to provide a custom handler, that function would invoke setEditorState, after performing something useful with the state:


const [editorState, setEditorState] = useState(() =>
    EditorState.createEmpty(),
);

const onEditorChange = (state) => {
   // all custom state manipulation happens here first  
   // const updatedState = do_something_interesting(state); 
    
    setEditorState(updatedState);
};
  
return (
    <Editor 
        editorState={editorState} 
        onChange={setEditorState} 
    />;
);

With Codox acting as the layer of data merging and synchronization between the outer container component and the underlying DraftJS editor component, this data flow requires a redirection. Given that the editor state may need to be updated in response to both local and remote input, the container component or the underlying editor must not directly update the editorState, and instead, they must delegate the update to codox.

This is done in two ways. First, by providing a custom onChange handler to the wrapped component, which explicitly invokes the Codox API function onEditorChange , like so:

const onEditorChange = (newEditorState) => {
  // all custom state manipulation happens here first  
  
  // … then delegate merging and updating editor to codox
  codoxProviderRef &&
  codoxProviderRef.current &&
  codoxProviderRef.current.onEditorChange(newEditorState);
 }
};
 

The only change here compared to the version with codox is to invoke codoxProviderRef.current.onEditorChange(newEditorState) instead of setEditorState(updatedState).

Second, the setEditorState function is passed as a prop to the wrapped component explicitly to allow codox to finally set the controlled editorState after any merging and updating operations are complete:

return (
   <EditorWithCodox
      //... other propse
      setEditorState={setEditorState}
    /> );

codoxProviderRef

Codox HOC provides an API layer to the outside world via forwardRef. This ref must be defined on the container component and passed in as a prop. The actual API instance is stored in the ref as ref.current

const codoxProviderRef = useRef();


<EditorWithCodox ref={codoxProviderRef}
	//...other props 
/>

For more usage guide, please refer to React documentation

Starting and Stopping Codox

React is opinionated about when and where side effects can take place in a React component's lifecycle, therefore some care is needed when invoking codox session start and stop functions.

We assume that when the application renders the editor wrapper, the latest content of the editor from the backend is either provided to the component (as a prop), or the wrapper will first perform an async fetch to obtain this content. Codox should be started after this latest content state is available for rendering. The document content is expected to be in raw JSON form, which can be transformed via the convertToDraw on a ContentState object.

The following code snippet illustrates the scenario where the container fetches the latest data via useEffect:

useEffect(() => {
   // fetch init state from BE
   fetch(“[backend_endpoint]”).then(({ state }) => {
   // pass fetched state to codox start
   codoxProviderRef.current.start(state);
    
   return () => {
    // call stop when the component is unmounted from dom
    codoxProviderRef.current.stop()
   }          
 }
});

When unmounting the editor component, invoke stop to terminate the connection to the sync service for the current user.

Rich-text and Plugins

Draftjs allows the developer to customize the basic vanilla editor with numerous plugins to support rich text editing, images, videos, @metions etc. Most plugins should work out of the box with Codox. A working example of a multiplayer Draft editor with rich-text support together with a myriad of plugins can be found here.

Last updated