---
title: React (Vite)
description: A runnable Vite and React 19 single-page app with the Templatical editor and no backend, the starting point for a React front end on any server.
---

# React (Vite)

[`examples/react-vite`](https://github.com/templatical/sdk/tree/main/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

[Open in StackBlitz](https://stackblitz.com/github/templatical/sdk/tree/main/examples/react-vite)

Or copy it into a new directory:

```bash
npx degit templatical/sdk/examples/react-vite my-app
cd my-app
npm install
npm run dev
```

## The 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 {#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](/frameworks/nextjs#providers) and implement the routes they call, in any language.

## Moving to production

- Pass a `templates` provider so templates live on your server instead of in one browser's `localStorage`.
- 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 the `mjml` package, or pass a `render` provider so `editor.toHtml()` calls your server. [Rendering & Export](/backend/render) covers both.
