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
import { useState } from "react";
import { DocxEditor } from "@eigenpal/docx-editor-react";
export function FileEditor() {
const [buf, setBuf] = useState<ArrayBuffer | null>(null);
return (
<>
<input
type="file"
accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
onChange={async (e) => {
const file = e.target.files?.[0];
if (file) setBuf(await file.arrayBuffer());
}}
/>
{buf && <DocxEditor documentBuffer={buf} />}
</>
);
}Autosave on change
Debounce onChange, persist the serialized buffer.
import { useRef, useCallback } from "react";
import { DocxEditor, type DocxEditorRef } from "@eigenpal/docx-editor-react";
function useDebouncedFn<T extends (...args: unknown[]) => void>(fn: T, ms: number) {
const timer = useRef<number | null>(null);
return useCallback(
(...args: Parameters<T>) => {
if (timer.current) window.clearTimeout(timer.current);
timer.current = window.setTimeout(() => fn(...args), ms);
},
[fn, ms],
);
}
export function AutosaveEditor({ docId }: { docId: string }) {
const ref = useRef<DocxEditorRef>(null);
const save = useDebouncedFn(async () => {
const buf = await ref.current?.save();
if (!buf) return;
await fetch(`/api/documents/${docId}`, { method: "PUT", body: buf });
}, 1500);
return <DocxEditor ref={ref} documentBuffer={null} onChange={save} />;
}Controlled comments
Own the comment array; mirror into a backing store.
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 { useState } from "react";
import { DocxEditor, type EditorMode } from "@eigenpal/docx-editor-react";
export function ReviewerEditor({ buf, reviewer }: { buf: ArrayBuffer; reviewer: string }) {
const [mode, setMode] = useState<EditorMode>("suggesting");
return (
<DocxEditor
documentBuffer={buf}
author={reviewer}
mode={mode}
onModeChange={setMode}
/>
);
}Edits made in "suggesting" mode wrap in revision markup. The owner of the document can accept or reject them later, individually or all at once, from the tracked-changes sidebar.
As of 1.1.0 the editor tracks more than inline text edits. Paragraph breaks, paragraph-property changes, table row/cell insert/delete/merge, inserted and deleted images, and list/numbering changes are all recorded as real revisions, shown in the sidebar, and round-tripped to Word's <w:ins> / <w:del> markup. Accept-all and reject-all resolve every revision type, and rejecting a list change cleanly reverts both the text and the numbering.
Custom toolbar
Hide the default toolbar, compose your own from the building blocks in /ui. EditorToolbar is the full composite the default chrome uses; Toolbar is the formatting-button strip on its own; ResponsiveToolbar adapts to width. Or build from ToolbarButton, ToolbarGroup, ToolbarSeparator.
import { DocxEditor } from "@eigenpal/docx-editor-react";
import {
Toolbar,
ToolbarButton,
ToolbarGroup,
ToolbarSeparator,
} from "@eigenpal/docx-editor-react/ui";
export function CustomChromeEditor({ buf }: { buf: ArrayBuffer }) {
return (
<div className="flex flex-col h-full">
<div className="border-b p-2 flex items-center justify-between">
<ToolbarGroup>
<ToolbarButton command="bold" />
<ToolbarButton command="italic" />
<ToolbarSeparator />
<ToolbarButton command="undo" />
</ToolbarGroup>
<SaveStatus />
</div>
<Toolbar />
<DocxEditor showToolbar={false} documentBuffer={buf} />
</div>
);
}In 0.x the formatting buttons were called FormattingBar. They're now Toolbar. The old composite is now EditorToolbar. See migration.
Custom fonts
Register self-hosted font files so the editor renders and measures them, then list the families in the picker. fonts injects the @font-face rules; fontFamilies controls the toolbar dropdown. Added in 1.1.0.
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)}
/>
);
}Each FontDefinition is { family, src, weight? }. src points at a woff2/woff/ttf/otf file you serve. The paths above (/fonts/...) resolve against your site root, so drop the files in your static directory (public/ in Next.js or Vite). Register multiple weights of one family as separate entries that share family. Match the family name to what your documents reference so the right glyphs paint. Font-load failures surface through onError (falling back to console.warn when no handler is attached).
For hosts that don't use the React or Vue adapter, drive the same registration through loadFontDefinitions from @eigenpal/docx-editor-core/utils.
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)
import * as Y from "yjs";
import { WebrtcProvider } from "y-webrtc";
import { ySyncPlugin, yCursorPlugin, yUndoPlugin } from "y-prosemirror";
import { DocxEditor } from "@eigenpal/docx-editor-react";
const ydoc = new Y.Doc();
const provider = new WebrtcProvider("room-1", ydoc);
const yXml = ydoc.getXmlFragment("docx");
const yComments = ydoc.getArray<Comment>("comments");
export function CollabEditor({ buf }: { buf: ArrayBuffer }) {
return (
<DocxEditor
documentBuffer={buf}
author={localStorage.getItem("name") ?? "Anonymous"}
externalPlugins={[
ySyncPlugin(yXml),
yCursorPlugin(provider.awareness),
yUndoPlugin(),
]}
comments={yComments.toArray()}
onCommentsChange={(next) => {
ydoc.transact(() => {
yComments.delete(0, yComments.length);
yComments.push(next);
});
}}
/>
);
}End-to-end walkthrough (provider choices, presence, comment sync) in Realtime collaboration.
Agent panel
The agentPanel.render slot renders a side panel next to the document.
import { useRef } from "react";
import { DocxEditor, type DocxEditorRef } from "@eigenpal/docx-editor-react";
import {
AgentChatLog,
AgentComposer,
useDocxAgentTools,
} from "@eigenpal/docx-editor-agents/react";
import { useChat } from "@ai-sdk/react";
export function EditorWithAgent({ buf }: { buf: ArrayBuffer }) {
const editorRef = useRef<DocxEditorRef>(null);
const { tools } = useDocxAgentTools({ editorRef });
const chat = useChat({ api: "/api/agent-chat", body: { tools } });
return (
<DocxEditor
ref={editorRef}
documentBuffer={buf}
agentPanel={{
render: () => (
<>
<AgentChatLog messages={chat.messages} />
<AgentComposer onSubmit={chat.sendMessage} />
</>
),
}}
/>
);
}Server-side route + full setup in Agents → Live editor.