Files

242 lines
7.4 KiB
Plaintext

---
title: "composeReactEmail"
sidebarTitle: "composeReactEmail"
description: "Convert editor content to email-ready HTML and plain text."
icon: "file-export"
---
The `composeReactEmail` function is the core of the editor's email export system. It takes
the editor's document tree, walks every node and mark, calls each extension's
`renderToReactEmail()` method, applies theme styles, wraps everything in an email-ready
template, and produces both HTML and plain text output.
## Import
```tsx
import { composeReactEmail } from '@react-email/editor/core';
```
## Signature
```tsx
async function composeReactEmail(params: {
editor: Editor;
preview: string | null;
}): Promise<{ html: string; text: string }>;
```
## Parameters
<ResponseField name="editor" type="Editor" required>
The TipTap editor instance. The function reads the editor's JSON document and walks through
each registered extension to serialize nodes and marks.
</ResponseField>
<ResponseField name="preview" type="string | null" required>
Preview text shown in inbox list views before the email is opened. Pass `null` to omit.
</ResponseField>
## Return value
Returns a `Promise` that resolves to an object with:
| Field | Type | Description |
|-------|------|-------------|
| `html` | `string` | Full HTML email string, ready to send |
| `text` | `string` | Plain text version for email clients that don't support HTML |
Both are generated in parallel for performance.
---
## The serialization pipeline
Understanding how `composeReactEmail` works helps you write better custom extensions and
debug rendering issues.
### 1. Extract document and extensions
The function reads the editor's JSON document (via `editor.getJSON()`) and collects all
registered extensions into a name-to-extension map for fast lookup.
### 2. Find the SerializerPlugin
It searches extensions for one that provides a `SerializerPlugin` — an interface with two
methods:
- **`getNodeStyles(node, depth, editor)`** — returns `React.CSSProperties` for a given node
- **`BaseTemplate({ previewText, children, editor })`** — wraps the serialized content in an email structure
The [`EmailTheming`](/editor/features/theming) extension implements this interface. If no
plugin is found, styles default to `{}` and the built-in `DefaultBaseTemplate` is used.
### 3. Traverse the document tree
It recursively walks the ProseMirror document. For each node it:
1. **Resolves styles** — calls `serializerPlugin.getNodeStyles(node, depth, editor)` to get
theme styles, then merges any inline styles from the node's attributes
2. **Renders unknown nodes as `null`** — if the node type isn't registered or isn't an
[`EmailNode`](/editor/api-reference/email-node), it returns `null`
3. **Renders the node** — calls the extension's `renderToReactEmail()` component, passing
`children` (from recursing into child nodes), `style`, `node`, and `extension`
4. **Wraps with marks** — iterates through the node's marks (bold, italic, link, etc.) and
wraps the rendered output with each mark's `renderToReactEmail()`
### 4. Depth tracking
Depth starts at `0` and only increments inside list nodes (`bulletList`, `orderedList`).
This enables different styling for nested vs. top-level elements — for example, paragraphs
inside list items use the `listParagraph` theme key instead of `paragraph`.
### 5. Style resolution order
Styles are resolved in this priority (highest wins):
1. **Inline styles** — styles set directly on a node via the editor (e.g., text alignment)
2. **Theme styles** — styles from the active theme via `getNodeStyles()`
3. **Extension defaults** — hardcoded styles in each extension's `renderToReactEmail()`
Inside each extension's renderer, these are typically merged:
```tsx
renderToReactEmail({ children, style, node }) {
return (
<p style={{
...style, // theme styles
...inlineCssToJs(node.attrs?.style), // inline overrides
}}>
{children}
</p>
);
}
```
### 6. Wrap in BaseTemplate
The serialized content is wrapped in a `BaseTemplate` that provides the email's
outer structure.
The **default template** renders:
```tsx
<Html>
<Head>
<meta content="width=device-width" name="viewport" />
<meta content="IE=edge" httpEquiv="X-UA-Compatible" />
<meta name="x-apple-disable-message-reformatting" />
<meta
content="telephone=no,address=no,email=no,date=no,url=no"
name="format-detection"
/>
</Head>
{previewText && <Preview>{previewText}</Preview>}
<Body>
<Section width="100%" align="center">
<Section style={{ width: '100%' }}>
{children}
</Section>
</Section>
</Body>
</Html>
```
When [`EmailTheming`](/editor/features/theming) is active, its `BaseTemplate` replaces the
default — it adds theme-specific body/container styles and can inject global CSS via a
`<style>` tag in the `<Head>`.
### 7. Render to HTML and plain text
The React tree is rendered to an HTML string using `react-email`' `render()`
function. Both the formatted HTML and a plain text version (tags stripped, text preserved)
are produced in parallel from the final React tree.
---
## Usage
### Basic export
```tsx
import { composeReactEmail } from '@react-email/editor/core';
const { html, text } = await composeReactEmail({ editor, preview: null });
```
### With preview text
The `preview` parameter sets the inbox preview snippet — the text shown before the email
is opened:
```tsx
const { html, text } = await composeReactEmail({
editor,
preview: 'Check out our latest updates!',
});
```
Pass `null` to omit preview text entirely.
### With theming
When the [`EmailTheming`](/editor/features/theming) extension is in your extensions array,
theme styles are automatically inlined into every node during export:
```tsx
import { StarterKit } from '@react-email/editor/extensions';
import { EmailTheming } from '@react-email/editor/plugins';
const extensions = [StarterKit, EmailTheming.configure({ theme: 'basic' })];
// Theme styles are injected automatically — no extra config needed
const { html } = await composeReactEmail({ editor, preview: null });
```
### Full example with export panel
```tsx
import { composeReactEmail } from '@react-email/editor/core';
import { useCurrentEditor } from '@tiptap/react';
import { useState } from 'react';
function ExportPanel() {
const { editor } = useCurrentEditor();
const [html, setHtml] = useState('');
const [exporting, setExporting] = useState(false);
const handleExport = async () => {
if (!editor) return;
setExporting(true);
const result = await composeReactEmail({ editor, preview: null });
setHtml(result.html);
setExporting(false);
};
return (
<div>
<button onClick={handleExport} disabled={exporting}>
{exporting ? 'Exporting...' : 'Export HTML'}
</button>
{html && (
<textarea
readOnly
value={html}
rows={16}
style={{ width: '100%', fontFamily: 'monospace' }}
/>
)}
</div>
);
}
```
---
## See also
- [`EmailNode`](/editor/api-reference/email-node) — defines how nodes serialize via `renderToReactEmail()`
- [`EmailMark`](/editor/api-reference/email-mark) — defines how marks serialize via `renderToReactEmail()`
- [Email Export](/editor/features/email-export) — guide with full editor + export examples
- [Theming](/editor/features/theming) — how `EmailTheming` provides styles and templates to the serializer