Skip to main content
ASoc
Tutorial

React Email Builder: Visual Editor, React Components, or a Plain String?

A React email builder is three different products: an embedded editor, a component library, or a plain string. The question that decides which one you need.

The ASoc Team9 min read

"React email builder" names three different products, and the search results mix them freely: a drag-and-drop editor you embed so non-developers can compose email, a code-first component library that compiles React to email-safe HTML, and a plain template string in a server file. Which one you need is decided by one question — who edits the email after launch.

This storefront sends three transactional emails and uses the third option: no email dependency at all, 210 lines across three files. That is not advice for every project. It is advice for email a developer writes once and nobody else touches. This post gives the decision rule, the real files on both sides of it, and the costs each option adds that its own documentation does not mention.

Three things called "a React email builder"

Visual editor SDKCode-first componentsTemplate string
ExamplesUnlayer React Email Editor, Beefree SDKReact Email, MJMLtext: / html: in a server file
Who authors a messageAnyone, in a browserA developer, in the repoA developer, in the repo
What it producesHTML plus a saved design (JSON)Compiled, inlined HTMLExactly what you typed
Ships to your users' browsersThe editor bundle, if you embed itNothingNothing
PreviewBuilt into the editorLocal dev previewSend yourself one
Review happens inA visual editorA pull requestA pull request
Earns its keep whenNon-developers change emailLayouts are rich and reusedThe email is six lines

The first column is a product your team logs into. The second is a build step. The third is a string. They are sold as the same category because all three answer "how do I make an email in React", but only the first one changes who is allowed to write copy — which is the expensive part of the decision, not the API.

The question that decides it

Who edits this email three months after launch?

If the answer is a marketer, a client, or anyone who will not open a pull request, you are buying an editor. Judge it on the storage format, not the component API: the editor saves a design as JSON and renders HTML from it, so the JSON is your content and it lives in the vendor's schema. Ask whether you can export it before you build on it.

If the answer is "a developer, in the repo", the editor UI is the one feature you will never use. What is left is the HTML problem — email clients ignore most modern CSS, and Outlook on Windows renders with Word's engine — and that problem only exists once your email has a layout. A single column of sentences has no layout to break.

That splits cleanly along transactional versus campaign email:

TransactionalCampaign / marketing
Distinct messagesSmall and fixedGrows every week
Written byA developer, onceWhoever owns the campaign
LayoutOne column, one linkHero image, columns, buttons
Changes becauseThe product changedThe offer changed
FitsA string, or componentsAn editor

Most "best React email builder" roundups are published by editor vendors, so they answer the campaign question. If you arrived at that search because an order confirmation needs sending, you are reading the wrong aisle.

What this storefront actually sends

Three emails, three files, no email package:

FileMessageLinesBody
src/lib/actions/contact.tsContact form, to support66text
src/lib/email/purchaseWelcome.tsPurchase confirmation, to the buyer72text
src/lib/email/refundRequest.tsNew refund request, to support72text

package.json carries 13 runtime dependencies — next, react, react-dom, the two Supabase clients, the MDX pipeline, lucide-react, @vercel/analytics, server-only. None of them is an email package. All three senders POST to the same endpoint with fetch; there is no SDK either.

Here is the whole compose step of the purchase confirmation, copied from src/lib/email/purchaseWelcome.ts:

const dashboardUrl = `${siteUrl()}/dashboard`;
const tierLabel = TIER_LABELS[tier];

const lines = [
  `Thanks for your purchase! Your ${tierLabel} plan is ready to download.`,
  "",
  `Go to your dashboard: ${dashboardUrl}`,
];
if (tier !== "t3") {
  lines.push(
    "",
    "Pick which template(s) you'd like to redeem from your dashboard.",
  );
}
const text = lines.join("\n");

And the send, which is the same shape in all three files — the two that mail support add a reply_to so a human can answer:

const res = await fetch("https://api.resend.com/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "ASoc <noreply@asoctemplates.com>",
    to: [email],
    subject: "Your ASoc downloads are ready",
    text,
  }),
});

Three things made the string the right answer here, and all three are properties of the email rather than of the tooling:

  1. The email's entire job is to carry one link. The buyer needs /dashboard. A hero image would not make the plan arrive faster.
  2. Plain text cannot render wrong. There is no Outlook bug for a paragraph. The one conditional in the body — the line about redeeming slots, which only non-bundle tiers see — is an if, not a template variable in a vendor's schema.
  3. It runs on a path that must not throw. The confirmation is sent from the order webhook after the entitlement transaction has committed, so every branch in that file resolves to void and logs on failure. A compile step is one more thing that can throw where nothing is allowed to.

The refund notification is the same shape with a different recipient and a reply_to set to the buyer, so support can answer by hitting reply. That is about fifteen lines of string building, and a builder would have added a template to maintain for it.

When a builder earns its keep

Three cases where the answer flips, honestly:

  • Someone outside the repo owns the copy. An embedded editor is the only option on this list that lets a marketer ship a change without a deploy. Everything else is theatre.
  • You have a dozen messages sharing a layout. This is React Email's case: a Layout, a Button, a Hero reused across templates, with table markup and inline styles generated for you. Hand-writing that HTML twelve times is how you end up with twelve subtly different headers.
  • Localisation or heavy personalisation. Components plus a data layer beat strings the moment the same message exists in five languages.

One cost the editor SDKs do not advertise: an embedded editor is a third-party script inside your app, so it has to pass your Content Security Policy. The CSP in this repo's next.config.ts names every external origin explicitly — adding an editor means a script-src entry, usually a frame-src entry for its iframe, and a retest of the page in production rather than in dev, which is where header problems hide. Budget that before you budget the integration.

The part no builder solves

A compose layer decides what the email says. It does not decide what happens when the send fails, and that is where transactional email actually breaks. This repo's own defect was exactly there: a send that was never awaited worked perfectly in development and silently dropped mail in production, because the serverless function froze the moment it returned a response. The fix is in transactional email in Next.js, along with the rule it produced — the contact form reports its failure to the user, the purchase confirmation swallows its own, and the choice is made per email by the caller, never by the template.

The other thing no builder solves is the sending domain. If the email must look like it came from you, the DNS records are yours to publish, whatever composes the body. EmailJS and what ships in your bundle covers the version of this where the send itself moves into the browser, and why that trade is worse than it looks.

Troubleshooting

SymptomLikely causeFix
Looks right in the editor preview, broken in OutlookThe preview renders in a browser engine; Outlook on Windows uses Word'sTest in the real client; keep the library's table markup instead of hand-editing it
The editor inflates an unrelated route's bundleImported at module scope in a shared client componentLoad it only in the route that needs it
Email arrives showing raw <p> tagsHTML passed in the text fieldSend html, and keep text as the plain-text alternative
Dev sends fine, production sends nothingThe send was not awaited; the function froze after the responseAwait it, or hand it to the platform's after-response API
A mail failure rolls back the purchaseThe send throws inside the transaction pathCatch and log in the sender; never let it reach the caller
Design unrecoverable after a vendor switchThe design JSON was never exportedStore the exported design alongside the message that uses it
Styles vanish in one client onlyA class-based stylesheet, not inlinedUse a library that inlines styles at compile time

Frequently asked questions

Is React Email a builder? Not in the drag-and-drop sense. It is a component library plus a local preview: you write React, it compiles to email-safe HTML with inline styles. Nobody outside your repo can author a message with it, which is the whole distinction this post turns on.

Can I use a visual email builder with the Next.js App Router? Yes, with the usual split — the editor is a client component (it is a DOM application), while the compile or save step belongs in a Server Action or route handler so no API key reaches the browser. The same boundary rule as any third-party editor.

Do I need a builder for one order confirmation? No. One confirmation is a string, as the 72-line file above shows. Reach for a builder on the second or third message that shares a layout, not before.

Can I reuse my Tailwind classes in email? Not directly — many clients drop class-based CSS, so a library has to inline the result at build time. The design tokens in this repo's src/app/globals.css are never used in email for that reason; the storefront's visual system stops at the browser, and the email is plain text on purpose.

Start from a finished front end

Email is the smallest surface in a product and the one most likely to be over-tooled. The page that earns the order is the one worth spending design time on, and that part you can inherit finished.

ASoc Fade is a barbershop website template, ASoc Fiscal a financial platform landing page, and ASoc Flow a workflow automation landing page — each a typed, statically rendered Next.js project you own outright.

Browse the full set of Next.js landing page templates, or the Tailwind landing page templates if you would rather bring your own framework.

Keep reading

Tutorial9 min read

React + EmailJS: What Ships in Your Bundle vs. a Server Action

EmailJS ships its service ID, template ID and public key in your bundle by design. This storefront's contact form ships none of that — the two files that decide it.

Read more
Tutorial10 min read

React Focus Trap: Two Dialogs Claimed aria-modal, One Meant It

An audit of four overlay surfaces: the wishlist panel that let Tab walk out of a modal dialog, the off-screen drawer pointer-events-none never hid, and the iframe case.

Read more
Tutorial10 min read

React Form Validation: The Allowlist Is the Validation

Client-side checks are UX. The server-side function that reads three named fields and ignores everything else — proven by a test that smuggles in a fake user_id — is the actual gate.

Read more