Fix: React Email and Resend Not Working — Emails Not Sending, Templates Not Rendering, or Styling Broken
Part of: React & Frontend Errors
Quick Answer
How to fix React Email and Resend issues — email template components, inline styles, Resend API integration, preview server, attachments, and email client compatibility.
The Problem
Resend returns a success response but no email arrives:
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
const { data, error } = await resend.emails.send({
from: '[email protected]',
to: '[email protected]',
subject: 'Welcome!',
html: '<h1>Hello</h1>',
});
// data.id exists but email never arrivesOr React Email components render in the preview but look broken in actual email clients:
import { Html, Body, Container, Text } from '@react-email/components';
function WelcomeEmail() {
return (
<Html>
<Body style={{ display: 'flex', justifyContent: 'center' }}>
<Container>
<Text>Welcome!</Text>
</Container>
</Body>
</Html>
);
}
// Preview looks perfect — Gmail shows a messOr the React Email preview server fails:
npx react-email dev
# Error: Cannot find module '@react-email/components'Why This Happens
React Email renders React components to HTML strings for email. Resend is the sending API. The issues are at different layers, and the underlying cause is rarely the layer the error message points at.
- Resend requires a verified domain — the
fromaddress must use a domain you’ve verified in the Resend dashboard. Using an unverified domain silently fails or goes to spam.[email protected]is the only default sender that works without verification. - Email clients strip most CSS — Gmail removes
<style>tags, Outlook doesn’t support flexbox or grid, and Apple Mail handles media queries differently. React Email components use inline styles and table-based layouts specifically because those are the only reliable approaches across email clients. - React Email is server-side only —
render()converts React Email components to HTML strings. This runs on the server (API route, Server Action). Trying to import React Email components in client-side code fails because some dependencies are Node.js-only. - The preview server needs the components installed —
npx react-email devscans for.tsxfiles in youremails/directory. If the directory structure or component imports are wrong, the preview server shows nothing.
Deliverability is its own opaque layer. Even with the correct API and a verified domain, an email can still fail to land in the inbox because of the recipient’s anti-spam stance, your sending IP’s reputation, or the regional mail provider’s policy. Resend uses Amazon SES under the hood for many regions, which means inherited SES reputation but also inherited SES restrictions — a freshly created domain starts in the SES sandbox-like state where deliverability is poor until you warm it up. Microsoft 365 and Outlook.com are particularly strict and often quarantine mail from new domains until a reputation builds, while Gmail is comparatively forgiving but downgrades messages without DKIM alignment.
React Email’s render function also depends on the runtime. The default render (from @react-email/render v0.0.12+) is async and uses a server-side HTML renderer. Older snippets call it synchronously and break under the new version. The Edge runtime can render simple templates but cannot run components that import Node-only packages like fs or @react-email/markdown (which depends on unified and pulls in CJS deps). When the same template renders fine locally and fails on Vercel Edge, this is usually why.
Fix 1: Set Up React Email Templates
npm install @react-email/components react-email resend// emails/welcome.tsx — email template
import {
Html,
Head,
Preview,
Body,
Container,
Section,
Text,
Link,
Img,
Button,
Hr,
Row,
Column,
} from '@react-email/components';
interface WelcomeEmailProps {
username: string;
loginUrl: string;
}
export default function WelcomeEmail({ username, loginUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
{/* Preview text — shown in inbox list, not in email body */}
<Preview>Welcome to MyApp, {username}!</Preview>
<Body style={main}>
<Container style={container}>
{/* Logo */}
<Img
src="https://myapp.com/logo.png"
width="120"
height="36"
alt="MyApp"
style={{ margin: '0 auto', display: 'block' }}
/>
<Text style={heading}>Welcome, {username}!</Text>
<Text style={paragraph}>
Your account has been created. Click the button below to get started.
</Text>
{/* CTA button — must use table-based layout for Outlook */}
<Section style={{ textAlign: 'center' as const, marginTop: '32px' }}>
<Button
href={loginUrl}
style={button}
>
Go to Dashboard
</Button>
</Section>
<Hr style={hr} />
<Text style={footer}>
If you didn't create this account, you can safely ignore this email.
</Text>
</Container>
</Body>
</Html>
);
}
// Styles must be inline objects — no CSS classes, no external stylesheets
const main = {
backgroundColor: '#f6f9fc',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
const container = {
backgroundColor: '#ffffff',
margin: '0 auto',
padding: '40px 20px',
maxWidth: '560px',
borderRadius: '8px',
};
const heading = {
fontSize: '24px',
fontWeight: '600' as const,
color: '#1a1a1a',
textAlign: 'center' as const,
margin: '30px 0',
};
const paragraph = {
fontSize: '16px',
lineHeight: '26px',
color: '#484848',
};
const button = {
backgroundColor: '#3b82f6',
borderRadius: '6px',
color: '#fff',
fontSize: '16px',
fontWeight: '600' as const,
textDecoration: 'none',
textAlign: 'center' as const,
display: 'inline-block',
padding: '12px 24px',
};
const hr = {
borderColor: '#e6ebf1',
margin: '32px 0',
};
const footer = {
color: '#8898aa',
fontSize: '12px',
lineHeight: '16px',
};Fix 2: Send Emails with Resend
# Set up environment variable
# RESEND_API_KEY=re_xxxxxxxxxx// lib/email.ts — Resend client
import { Resend } from 'resend';
import { render } from '@react-email/render';
import WelcomeEmail from '@/emails/welcome';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendWelcomeEmail(to: string, username: string) {
// Render React component to HTML string
const html = await render(WelcomeEmail({
username,
loginUrl: `https://myapp.com/login`,
}));
const { data, error } = await resend.emails.send({
from: 'MyApp <[email protected]>', // Must be verified domain
to,
subject: `Welcome to MyApp, ${username}!`,
html,
// Optional: plain text fallback
text: `Welcome to MyApp, ${username}! Visit https://myapp.com/login to get started.`,
// Optional: reply-to
replyTo: '[email protected]',
// Optional: tags for analytics
tags: [
{ name: 'category', value: 'welcome' },
],
});
if (error) {
console.error('Email send failed:', error);
throw new Error(`Failed to send email: ${error.message}`);
}
return data;
}
// Or pass the React component directly (Resend supports this)
export async function sendWelcomeEmailDirect(to: string, username: string) {
const { data, error } = await resend.emails.send({
from: 'MyApp <[email protected]>',
to,
subject: `Welcome to MyApp, ${username}!`,
react: WelcomeEmail({ username, loginUrl: 'https://myapp.com/login' }),
});
return { data, error };
}// app/api/send/route.ts — Next.js API route
import { sendWelcomeEmail } from '@/lib/email';
export async function POST(request: Request) {
const { email, username } = await request.json();
try {
const result = await sendWelcomeEmail(email, username);
return Response.json({ success: true, id: result?.id });
} catch (error) {
return Response.json({ error: 'Failed to send email' }, { status: 500 });
}
}Fix 3: Email-Safe Styling
Email clients have strict CSS support. Use these patterns:
import {
Html, Body, Container, Section, Row, Column, Text, Img,
} from '@react-email/components';
// Two-column layout — use Row/Column (renders as tables)
function NewsletterEmail({ articles }: { articles: Article[] }) {
return (
<Html>
<Body style={{ backgroundColor: '#f6f9fc', fontFamily: 'Arial, sans-serif' }}>
<Container style={{ maxWidth: '600px', margin: '0 auto' }}>
{/* Header with background color */}
<Section style={{
backgroundColor: '#1a1a2e',
padding: '40px 20px',
textAlign: 'center' as const,
borderRadius: '8px 8px 0 0',
}}>
<Text style={{ color: '#ffffff', fontSize: '28px', fontWeight: 'bold', margin: '0' }}>
Weekly Digest
</Text>
</Section>
{/* Two-column layout */}
<Section style={{ padding: '20px' }}>
<Row>
<Column style={{ width: '50%', paddingRight: '10px', verticalAlign: 'top' }}>
<Text style={{ fontWeight: 'bold', fontSize: '16px' }}>
{articles[0]?.title}
</Text>
<Text style={{ color: '#666', fontSize: '14px' }}>
{articles[0]?.excerpt}
</Text>
</Column>
<Column style={{ width: '50%', paddingLeft: '10px', verticalAlign: 'top' }}>
<Text style={{ fontWeight: 'bold', fontSize: '16px' }}>
{articles[1]?.title}
</Text>
<Text style={{ color: '#666', fontSize: '14px' }}>
{articles[1]?.excerpt}
</Text>
</Column>
</Row>
</Section>
{/* Safe CSS properties for email */}
{/*
Safe: color, background-color, font-size, font-weight, font-family,
text-align, padding, margin, border, width, height, line-height,
text-decoration, vertical-align, display (block/inline/none)
Avoid: flexbox, grid, position, float, box-shadow, border-radius (Outlook),
transform, animation, CSS variables, calc(), media queries (limited)
*/}
</Container>
</Body>
</Html>
);
}Fix 4: Resend Domain Verification
# Verify domain in Resend dashboard:
# 1. Go to https://resend.com/domains
# 2. Add your domain (e.g., myapp.com)
# 3. Add the DNS records (SPF, DKIM, DMARC) to your domain provider
# 4. Wait for verification (usually minutes, sometimes hours)DNS records you’ll need to add:
| Type | Name | Value | Purpose |
|---|---|---|---|
| TXT | @ or myapp.com | v=spf1 include:amazonses.com ~all | SPF |
| CNAME | resend._domainkey | Provided by Resend | DKIM |
| TXT | _dmarc | v=DMARC1; p=none; | DMARC |
Until domain is verified, use the test sender:
await resend.emails.send({
from: 'Acme <[email protected]>', // Works without verification
to: '[email protected]', // Test recipient
subject: 'Test',
html: '<p>Test email</p>',
});Fix 5: Preview and Development
# Start the React Email preview server
npx react-email dev
# Or add to package.json
# "email:dev": "react-email dev --dir emails --port 3001"# Expected directory structure
emails/
├── welcome.tsx
├── reset-password.tsx
├── order-confirmation.tsx
└── newsletter.tsx// Generate static HTML for testing
import { render } from '@react-email/render';
import WelcomeEmail from './emails/welcome';
const html = await render(WelcomeEmail({
username: 'Alice',
loginUrl: 'https://myapp.com/login',
}));
// Write to file for manual testing
import fs from 'fs';
fs.writeFileSync('test-email.html', html);
// Open test-email.html in a browser to previewFix 6: Attachments and Batch Sending
import { Resend } from 'resend';
import fs from 'fs';
const resend = new Resend(process.env.RESEND_API_KEY);
// Send with attachment
await resend.emails.send({
from: 'MyApp <[email protected]>',
to: '[email protected]',
subject: 'Your Invoice',
html: '<p>Please find your invoice attached.</p>',
attachments: [
{
filename: 'invoice.pdf',
content: fs.readFileSync('./invoices/inv-001.pdf'),
},
{
filename: 'receipt.pdf',
path: 'https://myapp.com/receipts/rec-001.pdf', // URL-based
},
],
});
// Batch send — multiple emails in one API call
const { data, error } = await resend.batch.send([
{
from: 'MyApp <[email protected]>',
to: '[email protected]',
subject: 'Hello Alice',
html: '<p>Hi Alice!</p>',
},
{
from: 'MyApp <[email protected]>',
to: '[email protected]',
subject: 'Hello Bob',
html: '<p>Hi Bob!</p>',
},
]);
// Up to 100 emails per batch
// Schedule email for later
await resend.emails.send({
from: 'MyApp <[email protected]>',
to: '[email protected]',
subject: 'Reminder',
html: '<p>Don\'t forget!</p>',
scheduledAt: '2024-12-25T09:00:00Z', // ISO 8601
});Fix 7: Platform Differences — Provider Choice, Region, Edge Runtime, Attachments
Email is one workload where the “platform” includes the sender, the recipient’s mail provider, the runtime that renders the template, and the geographic region the API call lands in. Each combination has its own quirks.
Provider comparison. Resend is the React-native option and the simplest API, but for established projects Postmark, AWS SES, and SendGrid still cover ground Resend doesn’t.
| Provider | Best for | Pricing | DKIM | Region |
|---|---|---|---|---|
| Resend | React/Next.js apps, transactional | 3000/mo free, $20/mo for 50k | Auto via dashboard | Global, US-East primary |
| Postmark | High-deliverability transactional | $15/mo for 10k | Dashboard wizard | US + EU separate streams |
| AWS SES | Bulk, low-cost, AWS-integrated | $0.10 per 1000 | Manual DNS | Per-region, sandbox by default |
| SendGrid | Marketing + transactional mixed | Free 100/day, paid tiers | DNS or single sender | Global |
| Nodemailer + SMTP | Self-hosted, full control | Server cost only | Self-managed | Wherever your server is |
If Resend is dropping mail to Microsoft 365 inboxes, fall back to Postmark for that segment. For self-hosted or low-volume scenarios SMTP-direct sending via Nodemailer covers the gap without an API layer.
Region matters. Resend’s primary infrastructure is US-East. If your users are in the EU and your sending pattern triggers a regional anti-spam profile, switching to a provider with EU-resident sending IPs (Postmark EU stream, SES EU) can fix delivery without any code change. The destination domain’s MX record gives you the recipient region for free — dig MX <domain> shows where mail is delivered.
Edge runtime rendering. Calling render() from a Next.js Server Action that runs on the Edge runtime works for plain templates but fails for any template that imports @react-email/markdown, dynamic image URLs that require node-fetch, or attachments that need fs. If the same template renders fine in next dev and crashes on Vercel Edge with Module not found: Can't resolve 'fs', move the send action to a Node runtime route (export const runtime = 'nodejs') or pre-render the HTML during build and send raw HTML.
Attachments. Resend accepts attachments as base64-encoded content or as a path URL. The path approach pulls the file from a public URL at send time — convenient but blocks if the URL is slow or the file is large. AWS SES has a 10 MB total message size limit including attachments; SendGrid is 30 MB; Postmark is 10 MB. Resend follows SES at 10 MB. For larger files, send a download link instead of attaching directly.
Image rendering in inboxes. Gmail proxies all <img> tags through https://googleusercontent.com, which strips referrers and caches images for hours. Outlook desktop blocks images by default until the user clicks “Download pictures.” This means a tracking pixel approach for open-rates dramatically undercounts Outlook opens, and any image with auth headers will appear broken in Gmail. Always use absolute HTTPS URLs and serve images from a CDN that allows no-referrer caching.
Still Not Working?
Email sends successfully but never arrives — check spam/junk folder first. Then verify your domain’s DNS records are correct in the Resend dashboard (all three: SPF, DKIM, DMARC should show green checkmarks). Without proper DNS, emails are flagged as spam or silently dropped by the recipient’s mail server.
React Email components render as plain text — you’re passing the component as a string instead of rendering it. Use render(MyEmail(props)) to convert the React component to an HTML string, or pass the component directly via Resend’s react property instead of html.
Styles look correct in preview but broken in Gmail — Gmail strips <style> tags and class attributes. All styles must be inline. React Email’s <style> component handles some inlining, but manual inline styles are most reliable. Also, Gmail doesn’t support border-radius in some contexts, background-image, or CSS variables.
render() throws “Cannot read properties of undefined” — the component might be using hooks or browser APIs. React Email templates must be pure render functions — no useState, no useEffect, no window. They’re rendered once on the server to produce a static HTML string.
Mail reaches Gmail but is rejected by Outlook/Microsoft 365 — Microsoft’s filtering is stricter on new domains and on SES-based senders. Add a _dmarc record with p=quarantine, ensure your From and Return-Path align, and warm the domain by sending small batches first. Consider routing Outlook recipients through Postmark while keeping Resend for other domains.
Edge runtime: “Cannot find module ‘fs’” or “stream is not a function” — React Email or one of its dependencies (markdown, image processing) needs Node. Add export const runtime = 'nodejs' to the route, or render the HTML at build time and ship a string instead of a component.
Resend returns 422 “From domain not verified” — only [email protected] works without verification. Any custom from domain must have a green checkmark on all DNS records in the dashboard. The verification check runs once on each send — adding DNS records and waiting up to 48 hours is sometimes required for newer top-level domains.
For related API and backend issues, see Fix: Auth.js Not Working, Fix: Hono Not Working, Fix: Nodemailer Not Working, and Fix: Vercel Edge Function Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Better Auth Not Working — Login Failing, Session Null, or OAuth Callback Error
How to fix Better Auth issues — server and client setup, email/password and OAuth providers, session management, middleware protection, database adapters, and plugin configuration.
Fix: next-safe-action Not Working — Action Not Executing, Validation Errors Missing, or Type Errors
How to fix next-safe-action issues — action client setup, Zod schema validation, useAction and useOptimisticAction hooks, middleware, error handling, and authorization patterns.
Fix: ts-rest Not Working — Contract Types Not Matching, Client Requests Failing, or Server Validation Errors
How to fix ts-rest issues — contract definition, type-safe client and server setup, Zod validation, Next.js App Router integration, error handling, and OpenAPI generation.
Fix: Auth.js (NextAuth) Not Working — Session Null, OAuth Callback Error, or CSRF Token Mismatch
How to fix Auth.js and NextAuth.js issues — OAuth provider setup, session handling in App Router and Pages Router, JWT vs database sessions, middleware protection, and credential provider configuration.