Code Tips9 min read

React Email 6 upgrade: imports, previews, and rendered HTML checks

Move an existing React Email 5 library to v6. Audit imports, check order and shipping fixtures, compare HTML and plain text, and verify the received emails.

R

React Emails Pro

September 25, 2026

TL;DR
  • React Email 6 moves components and rendering utilities into react-email.
  • The preview UI is a separate package. The visual editor is optional.
  • Review the HTML your order and shipping emails produce before releasing the dependency change.

An import migration can compile while a shipping email loses its tracking link. For an existing template library, the useful question is what your application still sends after the upgrade. Keep the dependency change small, give it realistic fixtures, and review the resulting messages before mixing in a redesign.

React Email 6 launched on April 17, 2026 with a unified package and a separate embeddable editor. This guide covers moving a working v5 library to v6. If you are still on an earlier version, start with the v5 and Tailwind 4 upgrade guide and review that change independently.


1. Decide which packages your app actually needs

The official migration guide moves components and render utilities into react-email. The old preview-server package becomes @react-email/ui. The editor remains a separate install. You do not need to embed an editor to keep sending templates defined in code.

Existing usev6 packageWhere to review it
Components and render utilitiesreact-emailTemplates, shared email components, send workers
Local preview interface@react-email/uiDeveloper tooling and preview deployment
An editor inside your product@react-email/editorOnly the app that exposes editing to users

Package roles in the v5-to-v6 migration.

Search your repository before uninstalling anything. Templates may import from the components package while a worker imports directly from the render package. A shared package can also re-export components under your own module name. Include those wrappers in the audit; a search limited to the emails directory will miss them.

terminal
rg -n '@react-email/|react-email'   --glob '*.{ts,tsx,js,jsx,json}'   --glob '!node_modules/**' --glob '!package-lock.json'

npm ls react-email @react-email/components @react-email/render   @react-email/preview-server @react-email/ui

Run the audit from the package or workspace that owns the email runtime. In a monorepo, inspect each sender separately. The website preview and the background worker may install different dependency sets even though they import the same template source.

Follow the official upgrade instructions using the package manager already used by that workspace, and keep the resulting lockfile in the change. For a specifically v6 migration, select a compatible v6 release rather than leaving a future install to resolve whatever major latest means then. Record the resolved versions in the review notes so another developer can reproduce the output.

If production renders emails at runtime, the package providing renderbelongs in that runtime's dependencies. A local preview can work even when a production install omits a package listed only in devDependencies.

Resist doing unrelated dependency cleanup in the same change. An upgrade that also replaces the email provider, changes the queue, and renames every template is difficult to review or roll back. A small package migration gives you a clear explanation for each changed line of generated HTML.


2. Move imports without changing the message

Update the import source in existing templates first. Preserve their props, copy, and styles while establishing the new baseline. If a component already works, this is a poor moment to also rename its fields or replace a table layout with a new abstraction.

The shipping notice below is a small v6 fixture, not a replacement for your full production template. It keeps the order identifier and tracking action easy to find. The function accepts complete display values so the same fixture can be passed to the preview and the render script without calling a database.

emails/shipping-notice.tsx
import * as React from "react";
import {
  Html, Head, Preview, Body, Container, Text, Button,
} from "react-email";

export interface ShippingNoticeProps {
  orderNumber: string;
  carrier: string;
  trackingUrl: string;
}

export default function ShippingNotice({
  orderNumber, carrier, trackingUrl,
}: ShippingNoticeProps) {
  return (
    <Html lang="en">
      <Head />
      <Preview>Tracking is available for order {orderNumber}.</Preview>
      <Body style={{ margin: 0, backgroundColor: "#f3f4f6" }}>
        <Container style={{
          maxWidth: "560px", padding: "24px",
          backgroundColor: "#ffffff", fontFamily: "Arial, sans-serif",
        }}>
          <Text style={{ color: "#111827", fontSize: "22px" }}>
            Order {orderNumber} is on its way
          </Text>
          <Text style={{ color: "#374151", fontSize: "16px" }}>
            {carrier} has your parcel. Use the tracking page for updates.
          </Text>
          <Button href={trackingUrl} style={{
            backgroundColor: "#1e40af", color: "#ffffff",
            padding: "14px 20px", fontSize: "16px",
          }}>
            Track your order
          </Button>
        </Container>
      </Body>
    </Html>
  );
}

ShippingNotice.PreviewProps = {
  orderNumber: "TEST-1042",
  carrier: "Example Carrier",
  trackingUrl: "https://example.com/tracking/TEST-1042",
} satisfies ShippingNoticeProps;

Use synthetic order numbers and customer details in migration fixtures. Keep them recognizable as test data. A realistic address copied from a support ticket can travel into screenshots, pull requests, and public preview deployments long after the upgrade is finished.

Preserve the business trigger too. The example says the carrier has the parcel, so the sending application should only use that wording when its fulfillment data supports it. If the event means only that a label was created, the copy needs to say that instead. Upgrading the renderer does not change what your order events mean.

Once the new imports compile, search again for old package references. Separate direct dependencies you can remove from transitive dependencies owned by another library. Do not force every transitive version to match with an override just to make the dependency tree look tidy. Investigate what still needs it.


3. Save HTML and plain text from fixed props

TypeScript checks the component contract. It does not tell you that the email still contains the right order number, that a tracking link survived a wrapper, or that the plain-text message is usable. Save both formats using a fixed fixture and compare them with the output from before the upgrade.

React Email's render documentation provides the HTML renderer and toPlainTextutility. This script uses those functions without contacting a delivery provider. Run it with your project's TypeScript runner, for example npx tsx scripts/check-shipping-email.tsx after adding tsx as a development dependency.

scripts/check-shipping-email.tsx
import * as React from "react";
import { strict as assert } from "node:assert";
import { mkdir, writeFile } from "node:fs/promises";
import { render, toPlainText } from "react-email";
import ShippingNotice from "../emails/shipping-notice";

async function main(): Promise<void> {
  const props = ShippingNotice.PreviewProps;
  const html = await render(<ShippingNotice {...props} />);
  const text = toPlainText(html);

  assert.match(html, /<!doctype/i);
  assert.ok(html.includes(props.orderNumber), "Missing order number");
  // This fixture URL has no characters that require HTML escaping.
  assert.ok(
    html.includes('href="' + props.trackingUrl + '"'),
    "Missing tracking link",
  );
  assert.ok(text.includes(props.orderNumber), "Missing plain-text order");
  assert.ok(text.includes(props.trackingUrl), "Missing plain-text link");
  assert.ok(!html.includes("undefined"), "Unexpected undefined value");

  await mkdir(".email-review", { recursive: true });
  await Promise.all([
    writeFile(".email-review/shipping.html", html, "utf8"),
    writeFile(".email-review/shipping.txt", text, "utf8"),
  ]);
  console.log("Shipping fixture rendered and checked");
}

main().catch((error: unknown) => {
  console.error(error);
  process.exitCode = 1;
});

These assertions are deliberately specific to the fixture. They catch a missing link or a missing order number; they do not certify the whole email. A template could pass them and still show a wrong amount or an unreadable button. Add checks for the promises your real template makes to its recipient.

For arbitrary URLs, use an HTML parser to inspect decoded attributes instead of copying the string assertion into a general validator. Query parameters may be escaped in the HTML while still representing the correct destination. A useful test distinguishes that serialization detail from a changed link.

Review the differences that affect a reader

Attribute ordering can change without changing the message. A missing tracking URL, hidden order total, or lost image alternative text deserves investigation. Save the files so the reviewer can inspect both the source and the visible result.

Include a long order identifier and a long carrier name in a second fixture. For your actual order confirmation, add multiple line items, a discount, and a quantity greater than one. The data should exercise the layout you already have. Keep currency and total calculations in the application layer so this renderer check is not quietly testing a new billing implementation at the same time.

If your store supports partial shipments, include an order with two parcels and check that each message names the correct shipment. A repeated order number is expected; a tracking URL copied from the other parcel is not. Keep that distinction in the fixture names so a reviewer can spot the mismatch without opening the order database.

Review plain text as a message somebody might actually read. It should state the order reference and provide the tracking destination without requiring the reader to infer what an unlabeled URL means. If the HTML relies on an image for a status label, the plain-text review is a good place to catch the missing explanation.


4. Check the preview, then the received message

Start the preview with your existing email development script after installing the new UI package. The CLI documentation describes template directories, preview props, and local static assets. In particular, assets served from the preview server are not automatically hosted for recipients. A local logo can look correct while its sent URL is unusable.

Check your image origins and link origins independently. Production image URLs should reach publicly accessible assets over HTTPS. Tracking links may lead to an authenticated application page, but the destination should still be the intended route. A successful local preview says nothing about whether a customer can access either URL.

Open the fixtures at narrow and wide widths. Check wrapping, button labels, and the relationship between the shipping status and its action. Compare screenshots with the baseline before adjusting spacing. If you change the layout to compensate for a rendering difference, describe that difference in the review so it can be checked separately.

Then send to internal test recipients through the real application path. A script that calls the renderer directly does not exercise the worker's installed dependencies, provider configuration, or link processing. Use a staging event with an unmistakable test subject. Keep customer queues out of the migration experiment.

Inspect the delivered message in the clients your customers use. Keep Gmail mobile and Gmail web as separate entries. Check classic Outlook for Windows separately from newer Outlook apps if both matter to your audience. Record the actual app versions tested instead of writing "Outlook passed" in the release notes.

A browser's dark preview is a useful local check. It does not reproduce every inbox transformation. If the delivered colors differ, use the Gmail dark-mode troubleshooting reference to isolate the failing element before changing your palette.


5. Keep the release and rollback easy to explain

Write down what changed: package versions, import sources, and any necessary markup adjustments. Attach the fixture output and a short record of received-message checks. That gives the reviewer something concrete to approve beyond a green type check.

Deploy the renderer and its dependency manifest together. In systems with separate web and worker deployments, confirm which service renders each email before the rollout. If the web app only enqueues props while the worker renders HTML, upgrading the web app alone does not upgrade the emails customers receive.

Keep the previous application artifact available for rollback. If a problem appears, decide whether it affects queued props or already rendered messages. A renderer rollback can change future output; it cannot alter HTML already stored in a queue or delivered to an inbox. Do not replay customer order events just to inspect a new template.

Watch the failures your application can actually report, such as rendering exceptions or rejected send requests. Rendering success does not prove inbox placement, and a delivery event does not prove the customer can read the button. Keep the visual checks alongside the operational checks rather than treating either as a replacement.

Once the migration is stable, review whether an embedded editor solves a real product need. Letting a merchant edit shipping copy introduces questions about allowed fields, preview data, and who can publish a change. Those deserve their own design. They do not need to hold up a straightforward upgrade of code-owned templates.

Key takeaway
  • Audit every rendering entry point, including background workers.
  • Compare HTML and plain text using fixed order and shipping fixtures.
  • Release after the received messages pass, with the previous artifact available.
R

React Emails Pro

Team

Building production-ready email templates with React Email. Writing about transactional email best practices, deliverability, and developer tooling.

Production-ready templates

Pick from 9 template packs built with React Email. One-time purchase, lifetime updates, tested across every major email client.

Browse all templates