@veltdev/draftjs-crdt library enables real-time collaborative editing on controlled Draft.js Editors. The collaboration editing engine is built on top of Yjs and Velt SDK.
Prerequisites
- Node.js (v14 or higher)
- React (v18 or higher)
- A Velt account with an API key (sign up)
- Optional: TypeScript for type safety
Setup
Step 1: Install Dependencies
Install the required packages:Step 2: Setup Velt
Initialize the Velt client by following the Velt Setup Docs. This is required for the collaboration engine to work.Step 3: Initialize Collaborative Editor
- Keep Draft.js
EditorStatein your application because Draft.js is a controlled editor. - Pass
getEditorStateandsetEditorStateaccessors to the collaboration hook or manager. - Route every Draft.js
onChangevalue throughhandleChange(). - Pass an
editorIdto uniquely identify each editor instance you have in your app.
- React Hook
- Imperative API
Use
useCollaboration() to manage Velt readiness, manager initialization, reactive status, versions, and cleanup.Step 4: Preserve the Controlled Editor Pattern
Keep a ref synchronized with the latestEditorState. This prevents stale closures in asynchronous manager callbacks.
Step 5: Status Monitoring (Optional)
The React hook returns reactivestatus, isSynced, and error state. For event-style subscriptions, use the manager:
Use connection status for UI feedback. Do not block local typing only because the status is
connecting; local edits still apply and sync when the provider reconnects.Step 6: Version Management (Optional)
The React hook keepsversions in state and provides convenience methods:
Step 7: Force Reset Initial Content (Optional)
initialContent can be a plain string or Draft.js RawDraftContentState. It is applied exactly once for a brand-new shared document.
Step 8: Configure Remote Cursors (Optional)
The manager publishes Draft.js selections through Yjs awareness but intentionally does not inject cursor DOM. Pass identity throughcursorData, subscribe to remote cursor changes, and render the cursor UI in your application.
RemoteDraftCursor contains the awareness client ID and can include the Velt user, custom cursor data, and serialized Draft selection.
Step 9: CRDT Event Subscription (Optional)
Listen for real-time CRDT data events usinggetCrdtElement().on() from the Velt client.
Step 10: Custom Encryption (Optional)
Encrypt CRDT data before it is stored in Velt by registering a custom encryption provider. For CRDT methods, input data is of typeUint8Array | number[].
- React / Next.js
- Imperative API
Step 11: Error Handling (Optional)
Pass anonError callback to handle non-fatal initialization and runtime errors. The manager returns safe defaults from getters and version methods when data is unavailable.
Step 12: Access Yjs Internals (Advanced)
The manager exposes escape hatches for advanced integrations.Step 13: Cleanup
The React hook destroys the manager automatically when the component unmounts or theeditorId changes. It also returns a destroy() method for early cleanup.
manager.destroy() during cleanup. Calling it more than once is safe.
Notes
- Controlled state: Keep one owner for
EditorStateand synchronize a ref with the latest state. - Required change pipeline: Route every Draft.js
onChangethroughhandleChange(). - Unique editorId: Users must share the same Velt document context and
editorIdto collaborate. - Initial content: Pass plain text or
RawDraftContentState; avoid converting existing Draft content through HTML. - Cursor rendering: The package publishes awareness data but leaves cursor DOM and positioning to the host application.
- Connection state: Do not disable local editing only because the provider is connecting.
- Lifecycle: The hook waits for Velt initialization and an authenticated user before creating the manager.
- Production authentication: Do not expose privileged server tokens in browser code; use Velt’s recommended server-backed authentication flow.
Testing and Debugging
To test collaboration:- Log in as two unique users in two separate browser profiles and open the same Velt document and Draft.js
editorIdin both. - Edit text, formatting, headings, or lists in one window and verify the updated snapshot appears in the other.
- Verify awareness cursor data arrives and your application renders the expected remote-user UI.
- Reload both windows and verify the shared document persists.
- Changes not syncing: Confirm every
EditoronChangevalue passes throughhandleChange(). - Stale remote state: Keep
getEditorState()backed by a ref containing the latest controlled state. - Editor not loading: Confirm the Velt client is initialized, the document is set, and the user is identified.
- Cursors not appearing: Subscribe to
onRemoteCursorsChange()and render the returned cursor data in your app. - Initial content overwriting edits: Do not enable
forceResetInitialContentduring normal page loads.
Complete Example
- React / Next.js
A complete collaborative Draft.js editor with controlled state, user login, status display, remote-user awareness, formatting, and version management.
Complete App.tsx
How It Works
- Your application owns
EditorStateand provides current-state and update accessors to the collaboration hook or manager. - The collaboration manager creates a Velt XML CRDT store, sync provider, Yjs document, awareness instance, and undo manager.
handleChange()serializes Draft raw content into the shared XML snapshot and returns the resolved controlled state.- Remote and restored snapshots are converted back into
EditorStateand applied throughsetEditorState. - Formatting and entities round-trip through Draft’s
RawDraftContentStaterepresentation. - Selections are published through awareness, while the host application controls cursor rendering and placement.
- Version management saves and restores named Yjs snapshots for every connected user.
- Cleanup removes listeners, clears awareness, and destroys the underlying store.
APIs
React: useCollaboration()
React lifecycle wrapper aroundcreateCollaboration(). Alias: useDraftJsCollaboration.
- Signature:
useCollaboration(config: UseCollaborationConfig): UseCollaborationReturn - Params:
editorId: Unique identifier for the collaborative editor.getEditorState: Returns the latest controlledEditorState.setEditorState: Applies remote, restored, or initialEditorState.veltClient: Optional explicit Velt client. Falls back toVeltProvidercontext.initialContent: Plain text orRawDraftContentStatefor a brand-new document.restContentKey: XML key used to bridge REST-created content. Default:'document-store'.cursorData: Name, color, avatar, and custom awareness fields.debounceMs: Throttle interval in milliseconds for backend writes.forceResetInitialContent: Replace existing shared content during initialization. Default:false.onError: Callback for non-fatal collaboration errors.
- Returns:
handleChange: Route every Draft.js change through this function.manager: UnderlyingCollaborationManager, ornullwhile loading.isLoading:trueuntil initialization completes.synced:trueafter initial backend sync completes.isSynced: Alias ofsynced.status:'connecting','connected', or'disconnected'.error: Most recent error, ornull.versions: Reactive saved-version list.saveVersion: Save a named version and refresh the list.restoreVersion: Restore a version by ID.refreshVersions: Refresh the saved-version list.destroy: Destroy collaboration early.
Imperative: createCollaboration()
Creates and initializes a manager. The promise resolves even when the manager enters a degraded state; non-fatal failures are reported throughonError.
- Signature:
createCollaboration(config: CollaborationConfig): Promise<CollaborationManager>
DraftCursorData
RemoteDraftCursor
VersionChangeEvent
Version
CollaborationManager Methods
manager.handleChange()
Serialize a local Draft.js change into the CRDT document and return the resolved controlled state.- Signature:
manager.handleChange(nextState: EditorState): EditorState
manager.sendCursorPosition()
Publish a Draft.js selection through awareness. When omitted, the manager uses the current editor selection.- Signature:
manager.sendCursorPosition(selection?: SelectionState | null): void
manager.getRemoteCursors()
Read the current remote awareness cursors.- Returns:
RemoteDraftCursor[]
manager.onRemoteCursorsChange()
Subscribe to remote cursor changes.- Signature:
manager.onRemoteCursorsChange(callback: (cursors: RemoteDraftCursor[]) => void): Unsubscribe
manager.onStatusChange()
Subscribe to connection status changes.- Signature:
manager.onStatusChange(callback: (status: SyncStatus) => void): Unsubscribe
manager.onSynced()
Subscribe to initial sync state changes.- Signature:
manager.onSynced(callback: (synced: boolean) => void): Unsubscribe
manager.initialized
Whether initialization has completed.- Returns:
boolean
manager.synced
Whether initial backend sync has completed.- Returns:
boolean
manager.status
Current connection status.- Returns:
SyncStatus('connecting' | 'connected' | 'disconnected')
manager.saveVersion()
Save a named snapshot and return its version ID.- Signature:
manager.saveVersion(name: string): Promise<string>
manager.getVersions()
List all saved versions.- Returns:
Promise<Version[]>
manager.restoreVersion()
Restore a version and hydrate the controlled Draft.js state.- Signature:
manager.restoreVersion(versionId: string): Promise<boolean>
manager.setStateFromVersion()
Apply aVersion object’s state to the shared document and Draft.js editor.
- Signature:
manager.setStateFromVersion(version: Version): Promise<void>
manager.onVersionsChange()
Subscribe to save, restore, and direct version-state events.- Signature:
manager.onVersionsChange(callback: (event: VersionChangeEvent) => void): Unsubscribe
manager.getDoc()
Get the underlying Yjs document.- Returns:
Y.Doc | null
manager.getXmlFragment()
Get the shared Draft.js XML fragment.- Returns:
Y.XmlFragment | null
manager.getXmlText()
Get the first Draft block’s sharedY.XmlText, when available.
- Returns:
Y.XmlText | null
manager.getProvider()
Get the Velt sync provider.- Returns:
SyncProvider | null
manager.getAwareness()
Get the Yjs awareness instance.- Returns:
Awareness | null
manager.getUndoManager()
Get the Yjs undo manager.- Returns:
Y.UndoManager | null
manager.getStore()
Get the underlying Velt CRDT store.- Returns:
Store<string> | null
manager.destroy()
Remove listeners, clear awareness, and destroy the store. Safe to call more than once.- Returns:
void
Custom Encryption
Encrypt CRDT data before it is stored in Velt by registering a custom encryption provider onVeltProvider or the imperative Velt client.
REST API Compatibility
- The wrapper uses a Velt CRDT store of type
xmlwith sourcedraftjs. - The primary Draft.js content key is
draftjs. - Full Draft raw JSON is stored in XML metadata, and each block also has a readable XML representation.
- The manager watches a second XML fragment for content created through the Velt CRDT REST API. Configure this with
restContentKey; the default is'document-store'. - REST-created content is bridged into the Draft.js editor, and local edits are mirrored back so REST reads stay current.
- Send Yjs-compatible XML state through CRDT REST endpoints; do not send raw Draft JSON unless the endpoint explicitly expects application data.
Concurrency and Data Model
Draft.js does not have a maintained operation-level Yjs binding. This wrapper therefore uses snapshot reconciliation:- The shared root is a
Y.XmlFragmentkeyed asdraftjs. - A
draftjs-metanode stores the full raw Draft JSON for exact fidelity. - Each Draft block is also represented as a
draftjs-blockelement with a childY.XmlText. - Online changes sync quickly, and formatting and block data round-trip through raw content.
- Offline changes converge after reconnect, but when two offline users edit the same old snapshot, one snapshot can supersede the other.

