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:
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 componentsimport { Editor, EditorState, convertFromRaw, convertToRaw } from "draft-js";
// import codox-providerimport { withCodox } from "codox-provider";
// decorate editorconst EditorWithCodox = withCodox(Editor, { convertFromRaw, convertToRaw });
// editor containerconst 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 optionsconst 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.