Loading and saving

Load DOCX bytes into the editor root, swap documents through the shared ref, and serialize the current state back out to a .docx file on demand.

The editor accepts .docx data and serializes the edited document to .docx. Load and save run in the browser. The editor does not upload or convert through a service.

Input formats

The editor takes the document through the document prop. The most common value is Uint8Array DOCX bytes:

<DocxEditor document={bytes} />

Start with an empty document

To open the editor on an empty page, pass 'blank'. It uses Calibri at 11 points. It also carries Word's built-in style gallery: Heading 1 through Heading 9, Title, Subtitle, Quote, No Spacing, and List Paragraph.

<DocxEditor document="blank" />

Omitting document differs from 'blank'. It means no document at all, so the editor shows its loading screen and every control stays disabled. Use undefined only while your own fetch is still running.

For a File > New command that a user can run more than once, call blankDocumentBytes() instead. 'blank' is a constant, so the editor treats a second 'blank' as the same document and keeps what the user typed. Fresh bytes replace it:

import { blankDocumentBytes } from '@docx-editor.dev/core/editor';

<button onClick={() => ref.current?.load(blankDocumentBytes())}>New</button>;

Call blankDocumentBytes() inside an event handler or into state, never inline in the document prop. It returns a new array each time, so an inline call rebuilds the editor on every render.

From a URL

Fetch the document bytes and handle loading failures before mounting the editor:

import { useEffect, useState } from 'react';
import { DocxEditor } from '@docx-editor.dev/react';

export function Editor({ url }: { url: string }) {
  const [doc, setDoc] = useState<Uint8Array>();
  const [error, setError] = useState<string>();

  useEffect(() => {
    const controller = new AbortController();
    setDoc(undefined);
    setError(undefined);

    async function load() {
      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error(`Load failed: ${response.status}`);
        const buffer = await response.arrayBuffer();
        if (!controller.signal.aborted) setDoc(new Uint8Array(buffer));
      } catch (cause) {
        if (!controller.signal.aborted) setError(String(cause));
      }
    }

    void load();
    return () => controller.abort();
  }, [url]);

  if (error) return <p role="alert">{error}</p>;
  return <DocxEditor document={doc} />;
}

From a file input

Read the selected file into bytes, then pass those bytes through document:

import { useState } from 'react';
import { DocxEditor } from '@docx-editor.dev/react';

export function FileEditor() {
  const [doc, setDoc] = useState<Uint8Array>();

  return (
    <>
      <input
        type="file"
        accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
        onChange={async (e) => {
          const file = e.target.files?.[0];
          if (!file) return;
          setDoc(new Uint8Array(await file.arrayBuffer()));
        }}
      />
      {doc && <DocxEditor document={doc} />}
    </>
  );
}

Replace a document at runtime

To replace the document without remounting the component, use the ref:

const ref = useRef<DocxEditorRef>(null);

ref.current?.load(nextBytes);

Save a document

Use one of these save paths:

  • save() on the ref returns Promise<ArrayBuffer | null>
  • The packaged File > Save action, which you override with onSave in React and the @save emit in Vue
<DocxEditor ref={ref} document={bytes} onSave={() => void persist()} />

save() on the ref resolves null when no editor is mounted, so guard the result. Editor.save() returns an ArrayBuffer on success and rejects when saving fails.

Field result refresh

Save validates pending protected form input and applies the field's format using the locale active when you entered the value. Invalid input rejects with code invalidArgs. The editor keeps the input and does not open an alert. Collaborative sessions reject saves that require form-field formatting.

Save updates stale, calibrated REF and NOTEREF results in the body, footnotes, and endnotes. The editor commits all updated parts as one undo step.

Locked fields and unsafe result structures keep their saved values. View mode, read-only sessions, and collaborative sessions do not rewrite field results.

See Fields and cross-references for supported switches and other limits.

Download helper

The serialized buffer downloads like any other binary. This helper is the same in both adapters, because both export the DocxEditorRef type:

async function downloadDocx(ref: DocxEditorRef, fileName: string) {
  const buf = await ref.save();
  if (!buf) return;
  const blob = new Blob([buf], {
    type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = fileName.endsWith('.docx') ? fileName : `${fileName}.docx`;
  a.click();
  URL.revokeObjectURL(url);
}

Autosave

Debounce the save call when the document reports a change. These examples use your application's document endpoint.

Render each autosave component with a key based on docId. This keeps its timers and pending saves associated with one document.

import { useEffect, useRef } from 'react';
import { DocxEditor, type DocxEditorRef } from '@docx-editor.dev/react';

export function AutosaveEditor({ docId, bytes }: { docId: string; bytes: Uint8Array }) {
  const ref = useRef<DocxEditorRef>(null);
  const timer = useRef<number | null>(null);

  useEffect(
    () => () => {
      if (timer.current !== null) window.clearTimeout(timer.current);
    },
    []
  );

  const onChange = () => {
    if (timer.current !== null) window.clearTimeout(timer.current);
    timer.current = window.setTimeout(async () => {
      try {
        const buf = await ref.current?.save();
        if (!buf) return;
        const response = await fetch(`/api/documents/${encodeURIComponent(docId)}`, {
          method: 'PUT',
          body: buf,
        });
        if (!response.ok) throw new Error(`Save failed: ${response.status}`);
      } catch (error) {
        console.error('Autosave failed', error);
      }
    }, 1500);
  };

  return <DocxEditor ref={ref} document={bytes} onChange={onChange} />;
}

Choose a debounce interval that matches your API's write limits. For overlapping requests, use server-side revision checks to reject stale writes. Show save failures in your application so users can retry.

Next steps

On this page