Loading and saving

Load DOCX bytes, replace documents, and save edits through the React and Vue editor APIs.

Pass DOCX bytes to the document prop to load a document. Call save() to get the edited DOCX bytes. Your application owns storage and network requests.

Load and save a document

This example loads a URL and downloads the saved file:

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

export function Editor({ url }: { url: string }) {
  const ref = useRef<DocxEditorRef>(null);
  const { document, error } = useDocxSource(url);

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

  if (error) return <p role="alert">The document failed to load.</p>;
  return (
    <>
      <button onClick={download}>Download</button>
      <DocxEditor ref={ref} document={document} />
    </>
  );
}

useDocxSource() fetches the bytes and ignores stale responses when the URL changes. If the bytes arrive but the editor cannot open them, see Handle files that do not open.

Save pathReturns
Ref save()Promise<ArrayBuffer | null>. Resolves null when no editor is mounted.
Core Editor.save()Promise<ArrayBuffer>. Rejects when saving fails.
File > SaveCalls onSave in React or emits save in Vue.

Choose a document source

document valueResult
Uint8ArrayOpens the DOCX bytes.
undefinedShows the loading screen and disables every control. Use it while you fetch bytes.
'blank'Opens an editable empty page in Calibri 11 pt with the standard built-in styles.

useDocxSource() accepts a URL string, URL, Uint8Array, or ArrayBuffer. For a file input, read the file into bytes first:

const file = input.files?.[0];
if (file) setDoc(new Uint8Array(await file.arrayBuffer()));

Show your own loading indicator

Pass loadingIndicator in React, or fill the #loadingIndicator slot in Vue. The editor shows it on the blank page while it reads the file, then below the laid-out pages while it lays out the rest.

  • Include screen reader text, such as a role="status" element.
  • The indicator needs the packaged chrome. With chrome={false}, see Custom loading screen.

Replace the open document

Call load() on the ref to replace the document without remounting:

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

ref.current?.load(nextBytes);
// A repeatable File > New command:
ref.current?.load(blankDocumentBytes());
  • Repeating 'blank' in document does not create another document, because the value does not change.
  • blankDocumentBytes() returns new bytes on each call, so call it in an event handler or store the result.
  • An inline blankDocumentBytes() call in document creates a new document identity and rebuilds the editor on every render.

Pass { direction: 'rtl' } for a right-to-left document.

To accept a complete updated file from a server, use the Document refresh API.

Save a document

A save captures the document when you call save(), then writes the file in short steps while the page stays responsive. Edits made during a save belong to the next save. Later saves reuse unchanged paragraphs and table rows, so they cost less than the first save.

Save also validates and updates fields:

  • Pending protected form input is validated and formatted. Invalid input rejects the save with invalidArgs.
  • Collaborative sessions reject saves that require form-field formatting.
  • Stale REF and NOTEREF results refresh as one undo step, so a save can change undo history.

For field rules, see Fields and cross-references.

Autosave safely

Debounce saves on change, run one save at a time, and save again if the document changed meanwhile:

const saving = useRef(false);
const again = useRef(false);

async function persist() {
  if (saving.current) {
    again.current = true;
    return;
  }
  saving.current = true;
  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) {
    showSaveError(error);
  } finally {
    saving.current = false;
    if (again.current) {
      again.current = false;
      void persist();
    }
  }
}
// Call persist() from a debounced onChange handler (Vue: @change).
  • Render the autosave component with a key based on docId, so timers and pending saves stay with one document.
  • Use server-side revision checks to reject stale writes.
  • Show save failures so users can retry.
  • Upload the ArrayBuffer as is; it is already compressed.

Limits

  • save() captures the document when it starts. The bytes describe that document even if you load another one before they are ready.
  • If save() waits for a pending edit and another document loads first, it rejects with invalidState (save.document-changed).
  • load() invalidates pending document refresh submissions.

Next steps