Frameworks

Remix

Add a Remix DOCX editor with a mount check and React.lazy. Keeps server rendering and hydration intact while the editor loads in the browser only.

This page builds a Remix route that loads a .docx, keeps SSR and hydration intact, and downloads the edited file.

Install

npm install @eigenpal/docx-editor-react

Mount the editor

Remix renders routes on the server by default and the editor is browser-only, so the route gates it behind a mount check plus a lazy() import. This is the pattern from examples/remix/app/routes/_index.tsx:

// app/routes/_index.tsx
import { lazy, Suspense, useEffect, useState } from 'react';

const Editor = lazy(() => import('../components/Editor').then((m) => ({ default: m.Editor })));

export default function Index() {
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);

  if (!mounted) return <div>Loading editor...</div>;

  return (
    <Suspense fallback={<div>Loading editor...</div>}>
      <Editor />
    </Suspense>
  );
}

The editor component is plain React, no framework-specific code:

// app/components/Editor.tsx
import { useRef, useState } from 'react';
import { DocxEditor, type DocxEditorRef } from '@eigenpal/docx-editor-react';
import '@eigenpal/docx-editor-react/styles.css';

export function Editor() {
  const editorRef = useRef<DocxEditorRef>(null);
  const [buffer, setBuffer] = useState<ArrayBuffer | null>(null);

  async function onFileSelect(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    if (file) setBuffer(await file.arrayBuffer());
  }

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

  return (
    <div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
      <div style={{ padding: 8, display: 'flex', gap: 8 }}>
        <input type="file" accept=".docx" onChange={onFileSelect} />
        <button onClick={onSave}>Save .docx</button>
      </div>
      <DocxEditor ref={editorRef} documentBuffer={buffer} showToolbar />
    </div>
  );
}

Why this pattern

The pattern solves two problems, one per half:

  • The mounted guard renders the same loading markup on the server and on the first client paint, so hydration never mismatches. Without it the editor would try to render during SSR and crash on window.
  • lazy() keeps the editor bundle out of the server build and out of the route's initial chunk.

Run the example

The example resolves @eigenpal/* from built output, so build the workspace packages once first:

git clone https://github.com/eigenpal/docx-editor.git
cd docx-editor
bun install
bun run build:packages
bun run dev:remix   # http://localhost:3001

Next steps

On this page