English
React (Vite)
examples/react-vite is a Vite and React 19 single-page app with the editor and no backend. It keeps the template in localStorage, stores saved blocks with the editor's createLocalStorageSavedBlocksProvider(), and renders MJML in the browser with @templatical/renderer. Use it as the starting point for a React front end whose backend is not JavaScript. CI builds the app and runs it in a browser against every change to the SDK.
Running the example
Or copy it into a new directory:
bash
npx degit templatical/sdk/examples/react-vite my-app
cd my-app
npm install
npm run devThe editor component
The component mounts the editor in an effect, and its cleanup unmounts it, including an editor that finishes loading after React StrictMode has cleaned the effect up. onChange writes every change to localStorage, and the next mount passes it back as content. If the editor fails to start, the toolbar shows the error message. It also shows errors the editor reports through onError, such as saved blocks it can't read from localStorage, and a failed export; the next export clears them.
src/email-editor.tsx
tsx
import { useEffect, useRef, useState } from "react";
import {
createLocalStorageSavedBlocksProvider,
init,
type TemplaticalEditor,
} from "@templatical/editor";
import type { TemplateContent } from "@templatical/types";
import "@templatical/editor/style.css";
// Everything here runs in the browser: the template is kept in localStorage,
// and saved blocks use the editor's own localStorage provider. To store them
// on a server instead, use the fetch-based providers from any full-stack
// example (lib/templatical/providers.ts in examples/nextjs).
const CONTENT_KEY = "templatical-example:content";
function storedContent(): TemplateContent | undefined {
try {
const raw = localStorage.getItem(CONTENT_KEY);
return raw ? (JSON.parse(raw) as TemplateContent) : undefined;
} catch {
return undefined;
}
}
const messageOf = (error: unknown) =>
error instanceof Error ? error.message : String(error);
export function EmailEditor() {
const containerRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<TemplaticalEditor | null>(null);
const [mjml, setMjml] = useState("");
const [problem, setProblem] = useState<string | null>(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
let cancelled = false;
let instance: TemplaticalEditor | null = null;
(async () => {
// In development, React StrictMode runs this effect, its cleanup and this
// effect again in one synchronous pass. Two init() calls on one
// container can finish in either order, and each replaces whatever the
// container holds when it finishes, so a cancelled run that finishes
// last leaves the container empty. Yielding once lets that cleanup
// cancel the first run before it calls init().
await Promise.resolve();
if (cancelled) return;
const ed = await init({
container,
content: storedContent(),
onChange(content) {
localStorage.setItem(CONTENT_KEY, JSON.stringify(content));
},
savedBlocks: createLocalStorageSavedBlocksProvider(),
onError: (error) => {
if (!cancelled) setProblem(error.message);
},
});
if (cancelled) {
ed.unmount();
return;
}
instance = ed;
editorRef.current = ed;
})().catch((error: unknown) => {
if (!cancelled) setProblem(messageOf(error));
});
return () => {
cancelled = true;
instance?.unmount();
editorRef.current = null;
};
}, []);
async function exportMjml() {
const ed = editorRef.current;
// No editor yet, or it failed to start: nothing to export, and a start
// failure keeps its alert.
if (!ed) return;
// A new export replaces an earlier failure.
setProblem(null);
try {
setMjml(await ed.toMjml());
} catch (error) {
setProblem(messageOf(error));
}
}
return (
<>
<div className="toolbar">
<button type="button" data-testid="export-mjml" onClick={exportMjml}>
Export MJML
</button>
{problem && <span role="alert">{problem}</span>}
</div>
<div className="workspace">
<div ref={containerRef} className="editor" />
{mjml && (
<pre data-testid="export-output" className="output">
{mjml}
</pre>
)}
</div>
</>
);
}The providers
This example passes savedBlocks, from createLocalStorageSavedBlocksProvider(). To keep templates and saved blocks on a server, pass the fetch-based providers from the Next.js example and implement the routes they call, in any language.
Moving to production
- Pass a
templatesprovider so templates live on your server instead of in one browser'slocalStorage. - Replace
createLocalStorageSavedBlocksProvider()with a provider backed by your server, so saved blocks follow the user across browsers. - Compile
editor.toMjml()'s output to HTML on your server with themjmlpackage, or pass arenderprovider soeditor.toHtml()calls your server. Rendering & Export covers both.