React

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.

Next steps

On this page