Skip to main content
The @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:
Import the Draft.js stylesheet in your application entry point:

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 EditorState in your application because Draft.js is a controlled editor.
  • Pass getEditorState and setEditorState accessors to the collaboration hook or manager.
  • Route every Draft.js onChange value through handleChange().
  • Pass an editorId to uniquely identify each editor instance you have in your app.
Use useCollaboration() to manage Velt readiness, manager initialization, reactive status, versions, and cleanup.
Route every local Draft.js change through the returned handleChange() function before updating your controlled state. Bypassing it prevents the change from reaching the CRDT document.

Step 4: Preserve the Controlled Editor Pattern

Keep a ref synchronized with the latest EditorState. This prevents stale closures in asynchronous manager callbacks.
Use normal Draft.js formatting utilities, but route their result through the same change handler:
Bold, italic, block types, list types, and entity ranges are preserved in the Draft raw-content snapshot.

Step 5: Status Monitoring (Optional)

The React hook returns reactive status, 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 keeps versions in state and provides convenience methods:
The same methods are available on the manager. After restore, the manager hydrates the controlled Draft.js state.

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.
Use forceResetInitialContent only for deliberate reset workflows. It replaces the existing shared Draft.js snapshot and clears current user edits.
To migrate existing Draft.js content, preserve its native raw representation:
Validate persisted raw JSON before passing it to the manager. Avoid converting through HTML because Draft raw content preserves editor semantics more accurately.

Step 8: Configure Remote Cursors (Optional)

The manager publishes Draft.js selections through Yjs awareness but intentionally does not inject cursor DOM. Pass identity through cursorData, subscribe to remote cursor changes, and render the cursor UI in your application.
Publish the current selection when focus changes:
Each 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 using getCrdtElement().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 type Uint8Array | number[].
See also: setEncryptionProvider() · VeltEncryptionProvider · EncryptConfig · DecryptConfig

Step 11: Error Handling (Optional)

Pass an onError 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 the editorId changes. It also returns a destroy() method for early cleanup.
When using the imperative API, call manager.destroy() during cleanup. Calling it more than once is safe.

Notes

  • Controlled state: Keep one owner for EditorState and synchronize a ref with the latest state.
  • Required change pipeline: Route every Draft.js onChange through handleChange().
  • Unique editorId: Users must share the same Velt document context and editorId to 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:
  1. Log in as two unique users in two separate browser profiles and open the same Velt document and Draft.js editorId in both.
  2. Edit text, formatting, headings, or lists in one window and verify the updated snapshot appears in the other.
  3. Verify awareness cursor data arrives and your application renders the expected remote-user UI.
  4. Reload both windows and verify the shared document persists.
Common issues:
  • Changes not syncing: Confirm every Editor onChange value passes through handleChange().
  • 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 forceResetInitialContent during normal page loads.
Enable browser console warnings to see detailed debugging information. You can also use the VeltCrdtStoreMap debugging interface to inspect and monitor your CRDT stores in real time.

Complete Example

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

  1. Your application owns EditorState and provides current-state and update accessors to the collaboration hook or manager.
  2. The collaboration manager creates a Velt XML CRDT store, sync provider, Yjs document, awareness instance, and undo manager.
  3. handleChange() serializes Draft raw content into the shared XML snapshot and returns the resolved controlled state.
  4. Remote and restored snapshots are converted back into EditorState and applied through setEditorState.
  5. Formatting and entities round-trip through Draft’s RawDraftContentState representation.
  6. Selections are published through awareness, while the host application controls cursor rendering and placement.
  7. Version management saves and restores named Yjs snapshots for every connected user.
  8. Cleanup removes listeners, clears awareness, and destroys the underlying store.

APIs

React: useCollaboration()

React lifecycle wrapper around createCollaboration(). Alias: useDraftJsCollaboration.
  • Signature: useCollaboration(config: UseCollaborationConfig): UseCollaborationReturn
  • Params:
    • editorId: Unique identifier for the collaborative editor.
    • getEditorState: Returns the latest controlled EditorState.
    • setEditorState: Applies remote, restored, or initial EditorState.
    • veltClient: Optional explicit Velt client. Falls back to VeltProvider context.
    • initialContent: Plain text or RawDraftContentState for 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: Underlying CollaborationManager, or null while loading.
    • isLoading: true until initialization completes.
    • synced: true after initial backend sync completes.
    • isSynced: Alias of synced.
    • status: 'connecting', 'connected', or 'disconnected'.
    • error: Most recent error, or null.
    • 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 through onError.
  • 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 a Version 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 shared Y.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 on VeltProvider or the imperative Velt client.

REST API Compatibility

  • The wrapper uses a Velt CRDT store of type xml with source draftjs.
  • 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.XmlFragment keyed as draftjs.
  • A draftjs-meta node stores the full raw Draft JSON for exact fidelity.
  • Each Draft block is also represented as a draftjs-block element with a child Y.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.
For character-level offline merging, use an editor with a maintained operation-level Yjs binding, such as Lexical or Slate.