Skip to content

DraftJS

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:

Terminal window
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.