Skip to content

Lexical Integration

WriteTrack includes a Lexical integration, compatible with Lexical v0.12+.

Install WriteTrack alongside Lexical:

Terminal window
npm i writetrack lexical
import { createEditor, $getSelection } from 'lexical';
import { createWriteTrackLexical } from 'writetrack/lexical';
const editor = createEditor({ namespace: 'MyEditor', onError: console.error });
editor.setRootElement(document.getElementById('editor'));
const handle = createWriteTrackLexical(editor, $getSelection, {
license: 'your-license-key',
});
// Access WriteTrack data when needed
const data = handle.tracker?.getData();

Most Lexical apps are built from plugin components under a <LexicalComposer> rather than createEditor(). WriteTrack slots in as one more plugin: grab the editor instance from the composer context and hand it to createWriteTrackLexical. @lexical/react’s <ContentEditable> calls editor.setRootElement() internally, so no manual root wiring is needed.

'use client'; // Next.js App Router
import { useEffect, useRef } from 'react';
import { $getSelection } from 'lexical';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { createWriteTrackLexical } from 'writetrack/lexical';
import type { WriteTrackLexicalHandle } from 'writetrack/lexical';
export function WriteTrackPlugin({
onReady,
}: {
onReady?: (handle: WriteTrackLexicalHandle) => void;
}) {
const [editor] = useLexicalComposerContext();
const handleRef = useRef<WriteTrackLexicalHandle | null>(null);
useEffect(() => {
const handle = createWriteTrackLexical(editor, $getSelection, {
contentId: 'article-body',
});
handleRef.current = handle;
onReady?.(handle);
return () => handle.destroy();
}, [editor]);
return null;
}

Render it inside the composer alongside your other plugins:

<LexicalComposer initialConfig={config}>
<RichTextPlugin contentEditable={<ContentEditable />} ... />
<WriteTrackPlugin onReady={(handle) => (trackerHandle = handle)} />
</LexicalComposer>

Pass $getSelection as the second argument and options as the third:

const handle = createWriteTrackLexical(editor, $getSelection, {
license: 'your-license-key',
userId: 'user-123',
contentId: 'document-456',
metadata: { formName: 'signup' },
autoStart: true, // default
});
OptionTypeDefaultDescription
licensestringundefinedWriteTrack license key
userIdstringundefinedUser identifier included in metadata
contentIdstringundefinedContent identifier included in metadata
metadataRecord<string, unknown>undefinedAdditional metadata
autoStartbooleantrueStart tracking when root element is available
wasmUrlstringundefinedURL to WASM binary for analysis
persistbooleanfalseEnable IndexedDB session persistence (requires contentId)
onTick(data: { activeTime: number; totalTime: number; tracker: WriteTrack }) => voidundefinedCallback fired every ~1s with active session time and tracker instance
onReady(tracker: WriteTrack) => voidundefinedCallback fired when tracker is ready (persistence loaded if applicable)
// Get the tracker instance (null until root element is available)
const tracker = handle.tracker;
// Get typing data
const data = tracker?.getData();
// Check tracking status
const isTracking = handle.isTracking;

If you prefer not to use the integration, you can attach WriteTrack directly to the root element:

import WriteTrack from 'writetrack';
import { createEditor } from 'lexical';
const editor = createEditor({ namespace: 'MyEditor', onError: console.error });
const rootElement = document.getElementById('editor')!;
editor.setRootElement(rootElement);
const tracker = new WriteTrack({
target: rootElement,
license: 'your-license-key',
});
tracker.start();

Call destroy() to stop tracking and release resources:

handle.destroy();

The integration also automatically cleans up if the root element is removed from the editor.

import type {
WriteTrackLexicalOptions,
WriteTrackLexicalHandle,
} from 'writetrack/lexical';