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" />Starting empty
To open the editor on an empty page, pass 'blank'. It is Word's blank template, with the
same Calibri 11pt defaults a new document in Word has. 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
import { useEffect, useState } from 'react';
import { DocxEditor } from '@docx-editor.dev/react';
export function Editor({ url }: { url: string }) {
const [doc, setDoc] = useState<Uint8Array>();
useEffect(() => {
let cancelled = false; // ignore stale responses if url changes
fetch(url)
.then((r) => r.arrayBuffer())
.then((buffer) => {
if (!cancelled) setDoc(new Uint8Array(buffer));
});
return () => {
cancelled = true;
};
}, [url]);
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.
Swapping documents 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);Saving
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:
import { 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);
const onChange = () => {
if (timer.current) window.clearTimeout(timer.current);
timer.current = window.setTimeout(async () => {
try {
const buf = await ref.current?.save();
if (!buf) return;
await fetch(`/api/documents/${docId}`, { method: 'PUT', body: buf });
} 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;
await fetch(`/api/documents/${props.docId}`, { method: 'PUT', body: buf });
} 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>Pick a debounce window that matches your backend's write tolerance. Use a 1–2 second debounce unless your API requires another interval.
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