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} /><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" /><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>;<script setup lang="ts">
import { ref } from 'vue';
import { DocxEditor, type DocxEditorRef } from '@docx-editor.dev/vue';
import { blankDocumentBytes } from '@docx-editor.dev/core/editor';
const editorRef = ref<DocxEditorRef | null>(null);
</script>
<template>
<button type="button" @click="editorRef?.load(blankDocumentBytes())">New</button>
<DocxEditor ref="editorRef" document="blank" />
</template>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} />;
}<script setup lang="ts">
import { DocxEditor, useDocxSource } from '@docx-editor.dev/vue';
const props = defineProps<{ url: string }>();
const { document, isLoading, error } = useDocxSource(() => props.url);
</script>
<template>
<p v-if="error">The document failed to load.</p>
<p v-else-if="isLoading">Loading…</p>
<DocxEditor v-else :document="document" />
</template>useDocxSource() fetches the bytes and tracks the request, so a changing URL never applies a stale response.
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} />}
</>
);
}<script setup lang="ts">
import { shallowRef } from 'vue';
import { DocxEditor } from '@docx-editor.dev/vue';
const doc = shallowRef<Uint8Array>();
async function pick(event: Event) {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
doc.value = new Uint8Array(await file.arrayBuffer());
}
</script>
<template>
<input
type="file"
accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
@change="pick"
/>
<DocxEditor v-if="doc" :document="doc" />
</template>useDocxSource() takes a URL string, a URL, a Uint8Array, or an ArrayBuffer. For a picked file, read the bytes first, as this sample does.
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);const editorRef = ref<DocxEditorRef | null>(null);
editorRef.value?.load(nextBytes);Save a document
Use one of these save paths:
save()on the ref returnsPromise<ArrayBuffer | null>- The packaged File > Save action, which you override with
onSavein React and the@saveemit in Vue
<DocxEditor ref={ref} document={bytes} onSave={() => void persist()} /><DocxEditor ref="editorRef" :document="bytes" @save="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} />;
}<script setup lang="ts">
import { onBeforeUnmount, ref } from 'vue';
import { DocxEditor, type DocxEditorRef } from '@docx-editor.dev/vue';
const props = defineProps<{ docId: string; bytes: Uint8Array }>();
const editorRef = ref<DocxEditorRef | null>(null);
let timer: number | null = null;
function onChange() {
if (timer !== null) window.clearTimeout(timer);
timer = window.setTimeout(async () => {
try {
const buf = await editorRef.value?.save();
if (!buf) return;
const response = await fetch(`/api/documents/${encodeURIComponent(props.docId)}`, {
method: 'PUT',
body: buf,
});
if (!response.ok) throw new Error(`Save failed: ${response.status}`);
} catch (error) {
console.error('Autosave failed', error);
}
}, 1500);
}
onBeforeUnmount(() => {
if (timer !== null) window.clearTimeout(timer);
});
</script>
<template>
<DocxEditor ref="editorRef" :document="bytes" @change="onChange" />
</template>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
- React props and Vue props for
document, the save action, and the shared ref - Editing API to read and write a document without mounting an editor
- React examples and Vue examples for more loading, saving, and editor state patterns