React Email10 min read

Preview Data Patterns for React Email: How to Test Edge Cases Before Production

Stop shipping emails that break with real data. Use preview data factories, Zod validation, edge case variants, and centralized registries to catch bugs during development.

R

React Emails Pro

March 1, 2026

React Email's preview mode is where most email bugs actually show up — not in production, but during development when you're testing edge cases.

The problem: preview data becomes an afterthought. You hardcode a couple of values, ship the template, and then production props drift away from what you tested.

The goal: Make preview data match production reality so closely that what you see during development is what users actually receive.

The problem with lazy preview data

Here's what typical preview data looks like:

emails/password-reset.tsx
// ❌ Bad: Minimal, unrealistic preview data
PasswordResetEmail.PreviewProps = {
  resetUrl: "https://example.com/reset",
  userName: "John",
};

This preview passes during development, but production sends:

  • Long URLs with tracking parameters
  • Edge case names (unicode, apostrophes, null values)
  • Optional fields that may or may not exist
  • Dynamic content based on user state

Result: Your email looks perfect in preview, breaks in production.


Pattern 1: Use a preview data factory

Instead of hardcoding props, create a factory function that generates realistic test data with variants.

lib/preview-data.ts
// ✅ Good: Factory with multiple variants
export function createPasswordResetPreview(
  variant: "default" | "long-name" | "tracked-url" = "default"
) {
  const base = {
    userName: "Jane Doe",
    resetUrl: "https://app.example.com/auth/reset?token=abc123",
    expiresInMinutes: 15,
  };

  const variants = {
    "default": base,
    "long-name": {
      ...base,
      userName: "María José Gutiérrez-O'Neill",
    },
    "tracked-url": {
      ...base,
      resetUrl:
        "https://app.example.com/auth/reset?token=abc123&utm_source=email&utm_campaign=password_reset&ref=notification",
    },
  };

  return variants[variant];
}

// Usage in your email template:
PasswordResetEmail.PreviewProps = createPasswordResetPreview();
Run npm run email dev and manually test each variant to catch layout issues (long names breaking buttons, URLs wrapping awkwardly, etc.).

Pattern 2: Mirror production schemas with Zod

If you validate props in production, use the same schema for preview data. This catches type mismatches before they ship.

emails/invoice.tsx
import { z } from "zod";

// Production schema
export const InvoiceEmailSchema = z.object({
  invoiceNumber: z.string(),
  customerName: z.string(),
  items: z.array(
    z.object({
      name: z.string(),
      quantity: z.number().int().positive(),
      price: z.number().positive(),
    })
  ),
  total: z.number().positive(),
  dueDate: z.string().datetime(),
});

export type InvoiceEmailProps = z.infer<typeof InvoiceEmailSchema>;

// Preview data validated at dev time
InvoiceEmail.PreviewProps = InvoiceEmailSchema.parse({
  invoiceNumber: "INV-2026-00142",
  customerName: "Acme Corp",
  items: [
    { name: "Pro Plan (Annual)", quantity: 1, price: 299.0 },
    { name: "Additional seat", quantity: 3, price: 29.0 },
  ],
  total: 386.0,
  dueDate: new Date("2026-03-15").toISOString(),
});

Now if production adds a new required field or changes a type, your preview breaks during development — not after users complain.


Pattern 3: Test with missing optional fields

Optional props are where runtime errors hide. Create preview variants that intentionally omit optionals to verify your fallbacks work.

emails/welcome.tsx
export function createWelcomePreview(
  variant: "full" | "minimal" = "full"
) {
  const full = {
    userName: "Alex Chen",
    companyName: "TechCorp",
    onboardingUrl: "https://app.example.com/onboarding",
    accountManagerName: "Sarah Johnson",
    accountManagerEmail: "sarah@example.com",
  };

  // Test what happens when optional fields are missing
  const minimal = {
    userName: full.userName,
    onboardingUrl: full.onboardingUrl,
    // No company, no account manager
  };

  return variant === "full" ? full : minimal;
}

// In your template component:
export default function WelcomeEmail({
  userName,
  companyName,
  onboardingUrl,
  accountManagerName,
}: WelcomeEmailProps) {
  return (
    <>
      <Heading>Welcome{companyName ? ` to ${companyName}` : ""}, {userName}!</Heading>

      <Button href={onboardingUrl}>Get Started</Button>

      {accountManagerName && (
        <Text>
          Your account manager, {accountManagerName}, will reach out within 24 hours.
        </Text>
      )}
    </>
  );
}
If you skip testing the minimal variant, you'll ship emails with undefined showing up in production copy.

Pattern 4: Centralize preview data registry

As your email count grows, preview data gets scattered. Create a central registry so you can test all emails from one place.

lib/preview-registry.ts
import { createPasswordResetPreview } from "./preview-data/auth";
import { createInvoicePreview } from "./preview-data/billing";
import { createWelcomePreview } from "./preview-data/onboarding";

export const PREVIEW_REGISTRY = {
  "password-reset": {
    default: createPasswordResetPreview(),
    "long-name": createPasswordResetPreview("long-name"),
    "tracked-url": createPasswordResetPreview("tracked-url"),
  },
  invoice: {
    default: createInvoicePreview(),
    "multi-item": createInvoicePreview("multi-item"),
  },
  welcome: {
    full: createWelcomePreview("full"),
    minimal: createWelcomePreview("minimal"),
  },
} as const;

// Usage: Loop through variants in CI or a test script
export function getAllPreviewVariants() {
  return Object.entries(PREVIEW_REGISTRY).flatMap(([templateId, variants]) =>
    Object.entries(variants).map(([variantId, props]) => ({
      templateId,
      variantId,
      props,
    }))
  );
}

Now you can write a script that renders every variant and catches layout issues before they ship.


Pattern 5: Snapshot testing with realistic data

Use preview data in your tests to catch regressions. If a change breaks the rendered HTML, your snapshot test fails.

__tests__/password-reset.test.tsx
import { render } from "@react-email/components";
import { describe, it, expect } from "vitest";
import PasswordResetEmail from "@/emails/password-reset";
import { createPasswordResetPreview } from "@/lib/preview-data";

describe("PasswordResetEmail", () => {
  it("renders with default preview data", () => {
    const html = render(<PasswordResetEmail {...createPasswordResetPreview()} />);
    expect(html).toMatchSnapshot();
  });

  it("handles long names without layout breaks", () => {
    const html = render(
      <PasswordResetEmail {...createPasswordResetPreview("long-name")} />
    );
    expect(html).toContain("María José Gutiérrez-O'Neill");
    expect(html).toMatchSnapshot();
  });

  it("renders tracked URLs correctly", () => {
    const props = createPasswordResetPreview("tracked-url");
    const html = render(<PasswordResetEmail {...props} />);
    expect(html).toContain(props.resetUrl);
  });
});
Run npm test before opening a PR. If your change alters the rendered HTML unexpectedly, the snapshot diff shows exactly what broke.

Common anti-patterns (and how to avoid them)

1) "lorem ipsum" in preview data

Placeholder text doesn't reveal layout issues. Use realistic data: real names, real product names, real-length copy.

  • ❌ Bad: userName: "User"
  • ✅ Good: userName: "Alejandra Rodríguez-Martínez"

2) No edge case variants

If you only test the happy path, edge cases break in production. Add variants for:

  • Empty arrays (no items in invoice, no notifications)
  • Long strings (URLs, addresses, product names)
  • Special characters (unicode, apostrophes, quotes)
  • Missing optional fields

3) Preview data that doesn't match production types

If you use TypeScript but don't validate preview data, you'll ship type mismatches. Always use the same type/schema for preview and production.


Implementation checklist

  • Create a lib/preview-data folder with factory functions
  • Use Zod schemas to validate preview props at dev time
  • Define at least 2-3 variants per template (default, edge case, minimal)
  • Centralize preview data in a registry for easy testing
  • Add snapshot tests that use realistic preview data
  • Manually preview each variant before shipping
Want production-ready templates with preview data already done? Check out our SaaS template library — every template includes realistic preview variants you can use as a reference.

Preview data is one piece of email reliability. Also see:

If your preview data matches production reality, you catch bugs during development instead of after users complain. That's the whole point.

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