@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. |
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
-
Storage adapters — persist templates and images to your own backend
-
Test email integration — implement
/api/send-test -
@maildeno/editorreference — fullEditorHandleAPI