Skip to content

DraftJS

This guide explains how to add multiplayer support to applications using React.js and the Draft.js editor.

Basic Integration

Start by adding draft-provider to your DraftJS project:

Terminal window
npm install @codoxhq/draft-provider

The following code snippet outlines how to establish basic integration between a Draft editor component and Codox:

import React, { useRef, useEffect, useState } from 'react';
// draftjs components
import {
Editor,
EditorState as DraftEditorState,
convertFromRaw,
convertToRaw,
} from 'draft-js';
// import codox provider
import { withCodox } from '@codoxhq/draft-provider';
// decorate editor
const EditorWithCodox = withCodox(Editor);
// editor container
const EditorContainer = () => {
// ref for codox api
const codoxAPI = useRef();
// flag to indicate codox started
const [codoxStarted, setCodoxStarted] = useState(false);
const [initStateReady, setInitStateReady] = useState(false);
const [currentDocId, setCurrentDocId] = useState(
'[document identifier]'
);
// local draft editor state
const [localEditorState, setEditorState] = useState(
DraftEditorState.createEmpty()
);
const fetchDocOnNetworkReconnect = async () => {
const { state, timestamp = Date.now() } =
await fetchDocStateByDocId(currentDocId);
// response must match the following schema:
return { state, timestamp };
};
const contentChangedHookCB = (data) => {
console.log(
'[contentChanged hook] hook invoked by Codox: ',
data
);
const { source, content } = data;
/**
* source can be one of "local", "remote"
* content is full lexical json state: root with comments
* Trigger save to db only for "local" changes
*/
if (source === 'local') {
(async () => {
// make http request here to update database
})();
}
};
//Codox config
const codoxConfig = {
docId: currentDocId,
username: 'user name',
apiKey: process.env.REACT_APP_CODOX_API_KEY, // codox api key
autoStart: true,
hooks: {
fetchDocOnNetworkReconnect,
contentChanged: contentChangedHookCB,
usersUpdate: (data) => {
console.log(
'[usersUpdate hook] hook invoked by Codox: ',
data
);
},
},
};
const setStartCodox = () => setCodoxStarted(true);
const setInitEditorState = (initJSONState) => {
// create init draft state out of json state
const draftContentState = convertFromRaw(initJSONState);
const draftState =
DraftEditorState.createWithContent(draftContentState);
setEditorState(draftState);
setInitStateReady(true);
};
// example useEffect to fetch document from some endpoint.
// codox should start after the latest document has been
// retrieved from the backend.
useEffect(() => {
(async () => {
try {
const initJSONState = await fetchDocInitStateByDocId(
currentDocId
);
setInitEditorState(initJSONState);
} catch (err) {
// capture errors here
}
})();
// will be called on unmount
return () => {
// stop codox when the component is unmounted
codoxAPI.current.stop();
};
}, []);
useEffect(() => {
if (!codoxStarted && initStateReady) {
codoxAPI.current
.start(codoxConfig)
.then(() => {
setStartCodox();
})
.catch((err) => console.log('codox.start error', err));
}
}, [initStateReady]);
// on editor change
const onEditorChange = (newEditorState) => {
// all custom state manipulation happens here first
// … then delegate merging and updating editor to codox
codoxAPI &&
codoxAPI.current &&
codoxAPI.current.onEditorChange(newEditorState);
};
// render
return (
<EditorWithCodox
// required props for codox
ref={codoxAPI}
editorState={localEditorState}
setEditorState={setEditorState}
// draft editor native props
onChange={onEditorChange}
// other draftjs options...
/>
);
};
export default EditorContainer;

Codox HOC

Codox provides the withCodox(DraftEditor) HOC to integrate multiplayer functionality into the Draft.js editor. This HOC automates merging and synchronization as editor states change due to local modifications or external updates. It manages the starting and stopping of the collaboration session (synchronization process).

import { withCodox } from '@codoxhq/draft-provider';
import { Editor } from 'draft-js';
const EditorContainer = () => {
const [localEditorState, setEditorState] = useState(
DraftEditorState.createEmpty()
);
// wrap Editor with codox.
const EditorWithCodox = withCodox(Editor);
// create ref for codox api
const codoxAPI = useRef();
// on editor change
const onEditorChange = (newEditorState) => {
// all custom state manipulation happens here first
// … then delegate merging and updating editor to codox
codoxAPI &&
codoxAPI.current &&
codoxAPI.current.onEditorChange(newEditorState);
};
return (
<EditorWithCodox
// required props for codox
ref={codoxAPI}
editorState={localEditorState}
setEditorState={setEditorState}
// draft editor native props
onChange={onEditorChange}
// other draftjs options...
/>
);
};

The withCodox HOC wraps a Draft.js Editor component, enabling it to receive and handle updates from both local user interactions and remote changes. It expects editorState, setEditorState, and onChange as props:

  • editorState: Current state of the editor.
  • setEditorState: Function to update the editor state after synchronization.
  • onChange: Handler for editor state changes.

Handling Editor State Changes

In a typical Draft.js implementation, the editor state is updated directly via the onChange handler provided to the Editor. For example:

const [editorState, setEditorState] = useState(() =>
DraftEditorState.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(() =>
DraftEditorState.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}
/>;
);

When integrating with Codox, the flow of updating the editor state is redirected to ensure that all local user’s and remote users’ inputs are reconciled correctly. This is achieved by:

  1. Custom onChange Handling: Instead of directly updating the editor state, this handler will invoke codoxAPI.current.onEditorChange, which manages the state internally and ensures that all changes (both local and remote) are synchronized.
const onEditorChange = (newEditorState) => {
// all custom state manipulation happens here first
// … then delegate merging and updating editor to codox
codoxAPI &&
codoxAPI.current &&
codoxAPI.current.onEditorChange(newEditorState);
};
  1. Setting Editor State: The setEditorState function is explicitly passed as a prop to the wrapped component. This setup allows Codox to apply the editor state post-merging and synchronization, ensuring that the editor reflects the most recent and accurate state of the document.
return (
<EditorWithCodox
//... other props
setEditorState={setEditorState}
/>
);

codoxAPI

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 codoxAPI = useRef();
<EditorWithCodox
ref={codoxAPI}
//...other props
/>;

For more usage guide, please refer to React documentation \

Starting and Stopping Codox

React’s lifecycle conventions dictate specific practices for handling side effects, making it crucial to consider when and how to initiate or terminate Codox sessions.

Typically, the editor wrapper component retrieves the latest document content from the backend, either directly as a prop or through an asynchronous fetch operation. It is essential that Codox is started only after this content is fully loaded and ready for rendering. The content should be provided in JSON format, suitable for transformation into Draft’s ContentState via convertToRaw.

The following snippet demonstrates fetching the latest document state and managing the Codox session lifecycle:

useEffect(() => {
(async () => {
try {
const initJSONState = await fetchDocInitStateByDocId(
currentDocId
);
setInitEditorState(initJSONState);
} catch (err) {
// capture errors here
}
})();
// will be called on unmount
return () => {
// stop codox when the component is unmounted
codoxAPI.current.stop();
};
}, []);
useEffect(() => {
/**
* When init state is ready, start codox with config
*/
if (initStateReady) {
codoxAPI.current
.start(codoxConfig)
.then(() => {
console.log('codox.start success');
})
.catch((err) => console.log(err));
}
}, [initStateReady]);

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

Rich-text and Plugins

Draft.js allows for extensive customization and enhancement through various plugins that support rich text features, images, videos, @mentions, and more. Most of these plugins are compatible with Codox right out of the box, allowing for a seamless integration into a multiplayer environment.

For a practical implementation example, visit the following GitHub repository which features a working example of a multiplayer Draft editor enriched with various plugins: Codox Draft Extended Example.

Events

Developers can tap into a variety of events emitted by Codox to handle synchronization states, user interactions, and errors more efficiently. It’s important to subscribe to these events before starting the Codox session to ensure all events are captured. Here’s how to integrate these event subscriptions into your application:

Preparing to Subscribe

Before initiating the Codox session with codoxAPI.start(), subscribe to the desired events. This setup ensures that your application is prepared to handle updates and changes from the moment the session begins.

useEffect(() => {
if (!codoxStarted && initStateReady) {
/**
* Subscribe to codox events.
* Must be invoked before .start() call
*/
// listen to content changes - emitted with synced state
codoxAPI.current.on('content_changed', (data) => {
// do own logic here, e.g. save to database
});
// listen to users update - emitted when remote users added/removed
codoxAPI.current.on('users_update', (data) => {
// do own logic here, e.g. display avatars somewhere
});
// listen to errors - emitted when errors occur
codoxAPI.current.on('error', (data) => {
// do own logic here
});
// run start after subscribint to events
codoxAPI.current
.start(codoxConfig)
.then(() => {
setStartCodox();
})
.catch((err) => console.log('codox.start error', err));
}
}, [initStateReady]);

Important Notes

  • Event Timing: Subscribing to events after invoking codoxAPI.start() may result in missing initial events or updates that occur right at the start of the session.
  • Handling Multiple Subscriptions: If your component could potentially re-subscribe due to re-rendering, ensure that you handle event subscription and unsubscription properly to avoid duplicate handlers.