Tutorial

Vue and React DOCX editor API comparison

Compare the Vue and React DOCX editor APIs: props, composables, hooks, refs, events, slots, and component composition.

3 min read

@docx-editor.dev/vue and @docx-editor.dev/react are two adapters over one engine. They share the OOXML parser, the command set, and the composition primitives.

This post covers the API differences between the adapters. For installation, loading, and saving, use the Vue DOCX editor guide or the React Word editor guide.

Compare the props

Both adapters take the same document and chrome props: document, mode, title, author, locale, i18n, t, fonts, zoom, zoomMode, menu, navigation, rulers, chrome, hyperlinkPopup, contextMenu, and modules. The document prop accepts an ArrayBuffer, a Uint8Array, a DocumentHandle, or the string 'blank'.

The adapters have four prop differences:

  • React takes callback props: onReady, onChange, onFontError, onSave, onOpen, and onTitleChange. Vue takes the first three as events on <DocxEditorRoot>, described in Events instead of callback props.
  • Vue adds colorMode, which accepts 'light', 'dark', or 'system'.
  • Styling uses className in React and class in Vue.
  • React accepts children as a prop. Vue uses slots.

Vue binds props with v-bind, so a prop with a value needs a colon:

<script setup lang="ts">
import { DocxEditor } from '@docx-editor.dev/vue';
import '@docx-editor.dev/vue/styles.css';
 
defineProps<{ bytes: Uint8Array }>();
</script>
 
<template>
  <DocxEditor
    :document="bytes"
    title="Q3 Memo"
    mode="view"
    :navigation="false"
  />
</template>

The React equivalent sets the same names:

import { DocxEditor } from '@docx-editor.dev/react';
import '@docx-editor.dev/core/styles/editor.css';
 
export function Viewer({ bytes }: { bytes: Uint8Array }) {
  return (
    <DocxEditor
      document={bytes}
      title="Q3 Memo"
      mode="view"
      navigation={false}
    />
  );
}

One difference is the stylesheet path. React documents @docx-editor.dev/core/styles/editor.css. Vue re-exports the same file as @docx-editor.dev/vue/styles.css.

Compare composables and hooks

Both adapters export the same helpers from the package root. Neither uses a /hooks or /composables subpath.

The shared set covers 36 names, including:

PurposeName
Read the editor instanceuseDocxEditor
Read editor stateuseEditorState, useEditorSnapshot, useEditorCaret
Run a commanduseEditorCommand, useEditorValueCommand
Subscribe to eventsuseEditorEvent
Page and paragraph setupusePageSetup, useParagraphIndent, useParagraphStyle
Fonts and zoomuseFontFamily, useFonts, useZoom
Navigation and searchuseDocumentOutline, useDocumentSearch, useNavigationPane
Content controlsuseContentControl, useContentControlInstance

React exports one name Vue does not: useProvidedDocxEditor. Every other helper has the same name in both adapters.

A toolbar button uses the same command API in both adapters. The Vue component uses this code:

<script setup lang="ts">
import { useEditorCommand } from '@docx-editor.dev/vue';
 
const bold = useEditorCommand('text.bold');
</script>
 
<template>
  <button
    @mousedown.prevent
    :disabled="!bold.isEnabled.value"
    :aria-pressed="bold.isActive.value"
    :title="bold.disabledReason.value ?? 'Bold'"
    @click="bold.execute()"
  >
    Bold
  </button>
</template>

Call the composable from a component rendered inside <DocxEditorRoot>. The composable accesses the editor through Vue provide/inject, so the component must be in that tree.

useEditorCommand returns execute plus three ComputedRef values. Vue unwraps only top-level refs in a template, and these sit on a returned object, so read them with .value. Writing bold.isEnabled gives the ref itself, which is always truthy.

Template refs instead of a ref object

React passes ref to the component and reads ref.current. Vue names the ref in the template and reads .value.

<script setup lang="ts">
import { ref } from 'vue';
import { DocxEditor, type DocxEditorRef } from '@docx-editor.dev/vue';
 
defineProps<{ bytes: Uint8Array }>();
 
const editorRef = ref<DocxEditorRef | null>(null);
 
async function save() {
  const buffer = await editorRef.value?.save();
  if (!buffer) return;
  await fetch('/api/documents/1', { method: 'PUT', body: buffer });
}
</script>
 
<template>
  <button @click="save">Save</button>
  <DocxEditor ref="editorRef" :document="bytes" />
</template>

DocxEditorRef exposes the same seven members in both adapters:

MemberReturns
load(document)void
save()Promise<ArrayBuffer | null>
getDocumentHandle()DocumentHandle | null
getEditor()Editor | null
focus()void
exec(command, options?)ExecResult
snapshot(options?)EditorSnapshot

The ref exposes only these seven members. Use getEditor() to access the full facade.

Events instead of callback props

React's onFoo callback props become Vue events on <DocxEditorRoot>. There are three:

React propVue eventPayload
onReady@readyEditor
onChange@changeDocumentChange
onFontError@font-errorEditorFontError
<script setup lang="ts">
import {
  DocxEditorContent,
  DocxEditorRoot,
  DocxEditorViewport,
} from '@docx-editor.dev/vue';
import type { DocumentChange } from '@docx-editor.dev/core';
 
defineProps<{ bytes: Uint8Array }>();
 
function onChange(change: DocumentChange) {
  console.log('document changed', change);
}
</script>
 
<template>
  <DocxEditorRoot :document="bytes" @change="onChange">
    <DocxEditorViewport>
      <DocxEditorContent />
    </DocxEditorViewport>
  </DocxEditorRoot>
</template>

Compose the chrome from parts

Both adapters expose the same parts. React nests them under the DocxEditor namespace, such as DocxEditor.Root. Vue imports them as top-level components:

<script setup lang="ts">
import {
  DocxEditorContent,
  DocxEditorRoot,
  DocxEditorToolbar,
  DocxEditorViewport,
} from '@docx-editor.dev/vue';
 
defineProps<{ bytes: Uint8Array }>();
</script>
 
<template>
  <DocxEditorRoot :document="bytes">
    <DocxEditorToolbar />
    <DocxEditorViewport>
      <DocxEditorContent />
    </DocxEditorViewport>
  </DocxEditorRoot>
</template>

Use the packaged <DocxEditor> when you need its default chrome. Compose parts for a custom layout or the Pro review rail.

Where to go next

.docx in. .docx out.

Open a real document in the editor, then wire it into your app. It all runs client-side.