@maildeno/editor doesn’t ship a React component — it doesn’t need to. init() mounts a self-contained custom element into any container you give it, so a small useRef + useEffect wrapper is all React-side integration takes.

This page covers plain React (Vite, CRA, or any client-rendered setup). For Next.js specifically, see Next.js — server rendering changes where the mount code is allowed to run.

Installation

npm install @maildeno/editor

The editor component

import { useEffect, useRef } from "react";
import { init, type EditorHandle } from "@maildeno/editor/init";

export default function Editor() {
  const container = useRef<HTMLDivElement>(null);
  const handle = useRef<EditorHandle | null>(null);

  useEffect(() => {
    let cancelled = false;

    init({
      container: container.current!,
      capabilities: { export: ["html", "mjml", "json"] },
      onSave: (payload: { templateId: string | null }) => {
        console.log("Saved", payload);
        const target = handle?.getHtml(); // handle?.getReactEmail() or handle?.getMjml()
        console.log(target);
      },
      onSendTestEmail: async ({ to, subject, html }) => {
        await fetch("/api/send-test", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ to, subject, html }),
        });
      },
    })

    return () => {
      cancelled = true;
      handle.current?.destroy();
      handle.current = null;
    };
  }, []);

  return <div ref={container} />;
}

The cancelled flag is the important part. In development, StrictMode runs the effect, cleans it up, then runs it again — without the guard you’d end up with two editors racing to occupy the same container, and the first handle reference would leak.

Reading exported output

Anything triggered from outside the component (a "Publish" button elsewhere in your app, for instance) just calls through the ref:

function PublishButton({ editorHandle }: { editorHandle: React.RefObject<EditorHandle | null> }) {
  const publish = async () => {
    const html = editorHandle.current?.getHtml();
    if (!html) return; // empty canvas
    await fetch("/api/campaigns", {
      method: "POST",
      body: JSON.stringify({ html }),
    });
  };

  return <button onClick={publish}>Publish</button>;
}

Where to go next