React examples
Concrete patterns for embedding <DocxEditor> in React: load from URL, controlled comments, autosave, custom toolbar, custom fonts, Yjs collaboration, agent panel.
Each example is a self-contained component. Drop into a React file, swap the data source, render.
Load a document from a URL
import { useEffect, useState } from 'react';
import { DocxEditor } from '@eigenpal/docx-editor-react';
import '@eigenpal/docx-editor-react/styles.css';
export function Editor({ url }: { url: string }) {
const [buf, setBuf] = useState<ArrayBuffer | null>(null);
useEffect(() => {
let cancelled = false;
fetch(url)
.then((r) => r.arrayBuffer())
.then((b) => {
if (!cancelled) setBuf(b);
});
return () => {
cancelled = true;
};
}, [url]);
return <DocxEditor documentBuffer={buf} />;
}null vs undefined: null mounts an empty document immediately; undefined defers the mount until the buffer arrives (skips the empty-state flash).
Load from a file input
documentBuffer accepts a File directly, so there is no arrayBuffer() step:
import { useState } from 'react';
import { DocxEditor } from '@eigenpal/docx-editor-react';
export function FileEditor() {
const [file, setFile] = useState<File | null>(null);
return (
<>
<input
type='file'
accept='.docx'
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
/>
{file && <DocxEditor documentBuffer={file} />}
</>
);
}Autosave
useAutoSave from /hooks persists the parsed document to localStorage on an interval (or debounced on change) and offers recovery after a crash or reload:
import { useState } from 'react';
import { DocxEditor } from '@eigenpal/docx-editor-react';
import { useAutoSave } from '@eigenpal/docx-editor-react/hooks';
import type { Document } from '@eigenpal/docx-editor-core';
export function AutosaveEditor() {
const [doc, setDoc] = useState<Document | null>(null);
const { status, lastSaveTime, hasRecoveryData, acceptRecovery } = useAutoSave(doc, {
saveOnChange: true,
});
return <DocxEditor documentBuffer={null} onChange={setDoc} />;
}To persist to a server instead, debounce save() on the ref yourself:
let timer: number | undefined;
function onChange() {
window.clearTimeout(timer);
timer = window.setTimeout(async () => {
const buf = await editorRef.current?.save(); // Promise<ArrayBuffer | null>
if (buf) await fetch(`/api/documents/${docId}`, { method: 'PUT', body: buf });
}, 1500);
}More on saving in Loading and saving.
Controlled comments
Own the comment array; mirror into a backing store. (React-only: the Vue adapter exposes comment events but not the comments prop.)
import { useState } from 'react';
import { DocxEditor } from '@eigenpal/docx-editor-react';
import type { Comment } from '@eigenpal/docx-editor-core';
export function ReviewEditor({ buf, author }: { buf: ArrayBuffer; author: string }) {
const [comments, setComments] = useState<Comment[]>([]);
return (
<DocxEditor
documentBuffer={buf}
author={author}
comments={comments}
onCommentsChange={(next) => {
setComments(next);
void fetch('/api/comments', {
method: 'PUT',
body: JSON.stringify(next),
headers: { 'content-type': 'application/json' },
});
}}
/>
);
}Suggesting mode (tracked changes)
import { DocxEditor } from '@eigenpal/docx-editor-react';
export function ReviewerEditor({ buf, reviewer }: { buf: ArrayBuffer; reviewer: string }) {
return <DocxEditor documentBuffer={buf} author={reviewer} mode='suggesting' />;
}Edits made in 'suggesting' mode wrap in revision markup; the document owner accepts or rejects them from the tracked-changes sidebar. Since 1.1.0 this covers paragraph, table, image, and list changes as well as inline text; see Tracked changes.
Custom toolbar
Hide the built-in toolbar and compose controls from /ui. ToolbarButton takes active, onClick, and children; drive both from the editor ref and onSelectionChange:
import { useRef, useState } from 'react';
import { DocxEditor, type DocxEditorRef } from '@eigenpal/docx-editor-react';
import { ToolbarButton, ToolbarGroup, ToolbarSeparator } from '@eigenpal/docx-editor-react/ui';
import type { SelectionState } from '@eigenpal/docx-editor-core/prosemirror';
export function CustomChromeEditor({ buf }: { buf: ArrayBuffer }) {
const editorRef = useRef<DocxEditorRef>(null);
const [selection, setSelection] = useState<SelectionState | null>(null);
function toggle(mark: 'bold' | 'italic') {
const info = editorRef.current?.getSelectionInfo();
if (!info?.paraId) return;
editorRef.current?.applyFormatting({
paraId: info.paraId,
search: info.selectedText || undefined,
marks: { [mark]: !selection?.textFormatting?.[mark] },
});
}
return (
<div className='flex flex-col h-full'>
<ToolbarGroup label='Formatting'>
<ToolbarButton
active={!!selection?.textFormatting?.bold}
onClick={() => toggle('bold')}
ariaLabel='Bold'
>
B
</ToolbarButton>
<ToolbarButton
active={!!selection?.textFormatting?.italic}
onClick={() => toggle('italic')}
ariaLabel='Italic'
>
I
</ToolbarButton>
<ToolbarSeparator />
</ToolbarGroup>
<DocxEditor
ref={editorRef}
documentBuffer={buf}
showToolbar={false}
onSelectionChange={setSelection}
/>
</div>
);
}Prefer the prebuilt strip? Keep showToolbar on and append your own controls with toolbarExtra, or compose the full EditorToolbar; both are covered in Toolbar customization.
In 0.x the formatting buttons were called FormattingBar. They're now Toolbar. The old composite is now EditorToolbar. See migration.
Custom fonts
fonts registers self-hosted font faces, fontFamilies controls the picker; the full rules are under Props → Fonts.
import { DocxEditor } from '@eigenpal/docx-editor-react';
import '@eigenpal/docx-editor-react/styles.css';
// Module-level so the array identity is stable across renders.
const FONTS = [
{ family: 'Inter', src: '/fonts/Inter-Regular.woff2' },
{ family: 'Inter', src: '/fonts/Inter-Bold.woff2', weight: 700 },
{ family: 'JetBrains Mono', src: '/fonts/JetBrainsMono-Regular.woff2' },
];
export function BrandedEditor({ buf }: { buf: ArrayBuffer }) {
return (
<DocxEditor
documentBuffer={buf}
fonts={FONTS}
fontFamilies={['Inter', 'JetBrains Mono', 'Arial', 'Times New Roman']}
onError={(err) => console.error('font or parse error', err)}
/>
);
}The /fonts/... paths resolve against your site root (public/ in Next.js or Vite).
Read-only viewer
import { DocxEditor } from '@eigenpal/docx-editor-react';
export function Viewer({ buf }: { buf: ArrayBuffer }) {
return (
<DocxEditor
documentBuffer={buf}
readOnly
showToolbar={false}
showZoomControl={false}
/>
);
}readOnly disables every edit affordance. Pair with showToolbar={false} for a viewer-only feel.
Realtime collaboration (Yjs)
Three props hand state to a CRDT: externalContent (skip the built-in loader; Yjs owns the content), externalPlugins (the y-prosemirror plugins), and comments + onCommentsChange (mirror threads into a Y.Array):
import { DocxEditor, createEmptyDocument } from '@eigenpal/docx-editor-react';
import { ySyncPlugin, yCursorPlugin, yUndoPlugin } from 'y-prosemirror';
<DocxEditor
document={createEmptyDocument()} // schema seed; Yjs populates the content
externalContent
externalPlugins={[ySyncPlugin(fragment), yCursorPlugin(provider.awareness), yUndoPlugin()]}
author={user.name}
/>;The provider setup, presence, and comment sync live on the canonical page: Realtime collaboration.
Agent panel
agentPanel={{ render }} mounts a resizable side panel next to the document, and useDocxAgentTools bridges tool calls to the live editor:
import { useDocxAgentTools } from '@eigenpal/docx-editor-agents/react';
const { tools, executeToolCall, getContext } = useDocxAgentTools({ editorRef });
<DocxEditor ref={editorRef} documentBuffer={buf} agentPanel={{ render: () => <MyChat /> }} />;The full loop (AI SDK transport, onToolCall execution, toAgentMessages for <AgentChatLog>, the server route) is the AI document editing tutorial; the bridge reference is Agents → Live editor.