react-email icon indicating copy to clipboard operation
react-email copied to clipboard

Error: A Node.js API is used (MessageChannel) which is not supported in the Edge Runtime.

Open nelsonlaidev opened this issue 1 year ago • 17 comments

Describe the Bug

Using React 19 + Next.js 15

I want to use @react-email/render on edge runtime. However, I got this error:

unhandledRejection: Error: A Node.js API is used (MessageChannel) which is not supported in the Edge Runtime.

Which package is affected (leave empty if unsure)

@react-email/render

Link to the code that reproduces this issue

https://codesandbox.io/p/devbox/react-email-render-on-edge-runtime-5dlkxp

To Reproduce

  1. Go to /api/email
  2. See the error

Expected Behavior

No errors. And render the component successfully in email.

What's your node version? (if relevant)

No response

nelsonlaidev avatar Aug 26 '24 14:08 nelsonlaidev

Not 100% sure we can do something about this as react-dom, which we use internally, seems to be the one using MessageChannel here. All the other APIs we use are supported on the edge without issues.

I've also noticed that the issue goes away when downgrading the React version and Next version to latest, so I do believe we will have support for MessageChannel and this issue going away soon. That being said, we should keep the issue open until it is fixed on Next. I suppose you can also downgrade React to an RC that did not use MessageChannel yet.

gabrielmfern avatar Aug 27 '24 20:08 gabrielmfern

@gabrielmfern , I’m not sure if supporting the Edge runtime as a separate environment, alongside browser and Node, is within the scope of react-email. However, if it is, you might find it helpful to review how the Next.js team addressed this issue here.

They used the react-dom/server.edge import to ensure compatibility between React and the Edge runtime.

shkreios avatar Sep 27 '24 15:09 shkreios

I’m not sure if supporting the Edge runtime as a separate environment, alongside browser and Node, is within the scope of react-email They used the react-dom/server.edge import to ensure compatibility between React and the Edge runtime.

Definitely within the scope. Thanks for letting us know about the react-dom/server.edge export, but that does seem to be exclusive to the newer RCs, so I don't think we can really use it yet until we remove support for 18.x.

gabrielmfern avatar Oct 14 '24 14:10 gabrielmfern

On another note, seems like Next 15 with edge routes also have this error when using the latest @react-email/render. Another possibility is that we can attempt at importing the edge version and fallback to other versions if it fails. Might need some finagling, but we'll need a way to have both versions working at the same time.

gabrielmfern avatar Oct 24 '24 19:10 gabrielmfern

Wrote my own render function using react-dom/server.edge

import type { ReactDOMServerReadableStream } from "react-dom/server";

const readStream = async (stream: ReactDOMServerReadableStream) => {
	let result = "";
	// means it's a readable stream
	const writableStream = new WritableStream({
		write(chunk) {
			result += new TextDecoder("utf-8").decode(chunk);
		},
	});
	await stream.pipeTo(writableStream);
	return result;
};

export const renderEdge = async (element: React.ReactNode) => {
	const renderToReadableStream = (await import("react-dom/server.edge"))
		.renderToReadableStream;
	const r = await renderToReadableStream(element);
	const html = await readStream(r);
	const doctype =
		'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
	const document = `${doctype}${html.replace(/<!DOCTYPE.*?>/, "")}`;
	return document;
};

It solves the problem but I encountered a new issue instead: TypeError: Cannot read properties of undefined (reading 'H')

It seems like React mail is not supporting NextJS 15 unfortunately. I'm not sure how to get past this.

In any case If it's complicated to support edge in the same render function due to backwards compatibility I would't mind having a separate render function for edge runtime. :)

cjkihl avatar Oct 25 '24 06:10 cjkihl

@cjkihl Noticed the same error when experimenting with the export. Looking at the compiled code, I am pretty sure this is because Next does its thing with pre-compiled React versions while still involving the user's React version, might be a bug on their side.

gabrielmfern avatar Oct 25 '24 14:10 gabrielmfern

Made an issue about the error when trying to render with react-dom/server.edge https://github.com/vercel/next.js/issues/71865

gabrielmfern avatar Oct 25 '24 16:10 gabrielmfern

Based on what I get from https://github.com/vercel/next.js/issues/71865#issuecomment-2444377726, this issue is probably only going to be fully fixed once we get react-markup fully implemented and released, unless we might adopt it while it is still experimental

gabrielmfern avatar Oct 31 '24 16:10 gabrielmfern

So I just got this issue today myself, but I've been using Pages Router and it wasn't happening until I tried to refactor to use App Router instead. In both scenarios, I have the function set to use Edge Runtime.

Is everybody here only using App Router? If so, does the issue resolve if you switch to Pages Router?

Edit: I just realized that somebody on the Next team confirmed this on the issue that @gabrielmfern logged there. So if you don't mind switching to Pages Router, that seems to be a temporary workaround.

dmarcucci avatar Nov 06 '24 20:11 dmarcucci

Is there a timeline for react-markup anywhere?

dualdetail avatar Nov 08 '24 21:11 dualdetail

@dualdetail Not that I know of, I don't think that they even announced it anywhere yet. But I assume it might release around the time React 19 is released.

gabrielmfern avatar Nov 10 '24 03:11 gabrielmfern

Not 100% sure we can do something about this as react-dom, which we use internally, seems to be the one using MessageChannel here. All the other APIs we use are supported on the edge without issues.

I've also noticed that the issue goes away when downgrading the React version and Next version to latest, so I do believe we will have support for MessageChannel and this issue going away soon. That being said, we should keep the issue open until it is fixed on Next. I suppose you can also downgrade React to an RC that did not use MessageChannel yet.

Stable versions of both React@19 and Next@15 are released, but this issue still exists, are there any fixes yet?

raodevendrasingh avatar Feb 07 '25 13:02 raodevendrasingh

@raodevendrasingh https://github.com/resend/react-email/issues/1630#issuecomment-2450289005

gabrielmfern avatar Feb 07 '25 15:02 gabrielmfern

Our project was recently migrated from React 18 & Next.js 14 to React 19 & Next.js 15. However, we're now stuck on sending emails due to this issue.

Has anyone found a workaround for this issue while waiting for the official fix or react-markup release? Any insights would be greatly appreciated!

rakibatomnixima avatar Feb 12 '25 05:02 rakibatomnixima

I faced exactly same issue while deploying my project to cloudflare. I am getting error as ReferenceError: MessageChannel is not defined I am using NextJS 15 with latest react version.

saurabhnemade avatar Feb 16 '25 21:02 saurabhnemade

Also hitting the same issue. Looks like we have to wait for Cloudflare support https://github.com/cloudflare/workerd/pull/3336

Gbuomprisco avatar Feb 26 '25 01:02 Gbuomprisco

I'm also encountering the "MessageChannel is not defined" error when using React DOM 19.0.0 in a server environment (Convex):

[Request ID: 634ac687fb526c49] Server Error
Uncaught ReferenceError: MessageChannel is not defined
    at <anonymous> (../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom-server.browser.development.js:7332:42)
    at node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom-server.browser.development.js (../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom-server.browser.development.js:8955:5)
    at __require (convex:/_deps/UZXTZDBD.js:9:50)
    at node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/server.browser.js (../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/server.browser.js:9:0)
    at __require (convex:/_deps/UZXTZDBD.js:9:50)
    at <anonymous> (../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/server.browser.js:17:0)

Environment:

  • React: 19.0.0
  • React DOM: 19.0.0
  • Package manager: pnpm
  • Environment: Convex backend

The error suggests that the browser-specific MessageChannel API is being referenced in a server environment where it's not available. This appears to be happening when attempting to use the react-dom/server module.

Has anyone else found a workaround? I'd appreciate any guidance on how to resolve this.

noobships avatar Mar 09 '25 20:03 noobships

im facing the same issue as @noobships . totally same environment

ibrahimyaacob92 avatar Mar 27 '25 15:03 ibrahimyaacob92

Possible work around is to run a prebuild script to regenerate the function with the html prerendered.

tsx scripts/prerender-emails.ts

// scripts/prerender-emails.ts
import fs from "fs";
import path from "path";
import { pathToFileURL } from "url";
import { render } from "@react-email/render";

const templatesDir = path.resolve(__dirname, "../emails/templates");
const outputDir = path.resolve(__dirname, "../emails/html");

interface TemplateModule {
  default: React.FC;
  props?: Record<string, string>;
}

/**
 * Converts a kebab-case or snake_case string to PascalCase
 * @param str The string to convert
 * @returns The PascalCase string
 */
const toPascalCase = (str: string): string =>
  str
    .replace(/[-_](.)/g, (_, group1) => group1.toUpperCase())
    .replace(/^(.)/, (_, group1) => group1.toUpperCase());

/**
 * Converts a PascalCase string to camelCase
 * @param str The PascalCase string to convert
 * @returns The camelCase string
 */
const toCamelCase = (str: string): string =>
  str.charAt(0).toLowerCase() + str.slice(1);

async function prerenderTemplates() {
  /**
   * Prerenders React Email templates to HTML files with placeholder values.
   *
   * This script:
   * 1. Reads all .tsx template files from the templates directory
   * 2. Imports and renders each template with placeholder props
   * 3. Generates HTML files with template literal placeholders
   * 4. Saves the HTML files to the output directory
   *
   * The generated HTML files can then be used with template literals
   * to inject real values at runtime.
   */

  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true });
  }

  const files = fs.readdirSync(templatesDir).filter((f) => f.endsWith(".tsx"));

  for (const file of files) {
    const baseName = file.replace(/\.tsx$/, "");
    const pascalName = toPascalCase(baseName);
    const camelName = toCamelCase(pascalName);
    const modulePath = pathToFileURL(path.join(templatesDir, file)).href;

    let mod: TemplateModule;
    try {
      mod = (await import(modulePath)) as TemplateModule;
    } catch (err) {
      console.error(`❌ Failed to import ${file}:`, err);
      continue;
    }

    const Component = mod.default;
    const props = mod.props ?? {};

    if (!Component) {
      console.warn(`⚠️  Skipping ${file} — no default export found.`);
      continue;
    }

    let html: string;
    try {
      html = await render(Component(props) as React.ReactElement);
    } catch (err) {
      console.error(`❌ Failed to render ${file}:`, err);
      continue;
    }

    const escapedHtml = html.replace(/`/g, "\\`");

    const propKeys = Object.keys(props);
    const destructure = propKeys.length
      ? `const { ${propKeys
          .map((k) => `${k} = "${props[k]}"`)
          .join(", ")} } = props;`
      : "";

    let htmlWithPlaceholders = escapedHtml;
    for (const key of propKeys) {
      htmlWithPlaceholders = htmlWithPlaceholders.replace(
        new RegExp(`{{${key}}}`, "g"),
        `\${${key}}`,
      );
    }

    const templateFn = `// ⚠️ Auto-generated by scripts/prerender-emails.ts
export const ${camelName}Template = (props: Record<string, string | number> = {}): string => {
  ${destructure}
  return \`${htmlWithPlaceholders}\`;
};
`;

    const outputFilePath = path.join(outputDir, `${pascalName}.ts`);
    fs.writeFileSync(outputFilePath, templateFn, "utf8");

    console.log(
      `✅ Generated: ${path.relative(process.cwd(), outputFilePath)}`,
    );
  }
}

prerenderTemplates().catch((err) => {
  console.error("Unhandled error in prerendering:", err);
  process.exit(1);
});
// emails/utils.ts
export const definePlaceholderProps = <T>(props: Record<keyof T, string>) =>
  props;

// emails/templates/verification-code.tsx
import React from "react";
import {
  Html,
  Tailwind,
  Head,
  Container,
  Section,
  Heading,
  Text,
} from "@react-email/components";
import { definePlaceholderProps } from "../utils";

export type VerificationCodeProps = {
  token: string;
  minutesUntilExpiry: string | number;
};

export const props = definePlaceholderProps<VerificationCodeProps>({
  token: "{{token}}",
  minutesUntilExpiry: "{{minutesUntilExpiry}}",
});

export default function VerificationCodeEmail({
  token,
  minutesUntilExpiry,
}: VerificationCodeProps) {
  return (
    <Html>
      <Tailwind>
        <Head />
        <Container className="px-20 font-sans">
          <Heading className="text-xl font-bold mb-4">
            Sign in to My App
          </Heading>
          <Text className="text-sm">Please enter the following code:</Text>
          <Section className="text-center">
            <Text className="font-semibold">Verification code</Text>
            <Text className="font-bold text-4xl">{token}</Text>
            <Text>(This code is valid for {minutesUntilExpiry} minutes)</Text>
          </Section>
        </Container>
      </Tailwind>
    </Html>
  );
}

liamb13 avatar Mar 30 '25 15:03 liamb13

You can polyfill MessageChannel, it doesn't appear to be actually used. Can render emails in Convex runtime (a web-like runtime similar to Edge) with React 19 using this polyfill and the patch outlined below. A more minimal polyfill may work, didn't test that far.

if (typeof MessageChannel === "undefined") {
  class MockMessagePort {
    onmessage: ((ev: MessageEvent) => void) | undefined;
    onmessageerror: ((ev: MessageEvent) => void) | undefined;

    close() {}
    postMessage(_message: unknown, _transfer: Transferable[] = []) {}
    start() {}
    addEventListener() {}
    removeEventListener() {}
    dispatchEvent(_event: Event): boolean {
      return false;
    }
  }

  class MockMessageChannel {
    port1: MockMessagePort;
    port2: MockMessagePort;

    constructor() {
      this.port1 = new MockMessagePort();
      this.port2 = new MockMessagePort();
    }
  }

  globalThis.MessageChannel =
    MockMessageChannel as unknown as typeof MessageChannel;
}

With this solved, I'm seeing @react-email/render/dist/node/index.mjs attempting to run reactDOMServer.renderToReadableStream, when the actual import is reactDOMServer.default.renderToReadableStream. I don't know if this is just a bundling issue for react-email or something deeper, but patching fixes it.

erquhart avatar Apr 02 '25 18:04 erquhart

With this solved, I'm seeing @react-email/render/dist/node/index.mjs attempting to run reactDOMServer.renderToReadableStream, when the actual import is reactDOMServer.default.renderToReadableStream. I don't know if this is just a bundling issue for react-email or something deeper, but patching fixes it.

#2221 fixes this

MendyLanda avatar May 06 '25 18:05 MendyLanda

Still not working even after resends merge, using convex and react-email

JClackett avatar Jul 16 '25 11:07 JClackett

Yeah this issue is now pending this PR (which is indefinitely in draft) as I understand it: https://github.com/resend/react-email/pull/2225

@JClackett in case you're blocked, there's a workaround for Convex: https://discord.com/channels/1019350475847499849/1328374779635302510/1385305749705457846

erquhart avatar Jul 16 '25 15:07 erquhart

@rakibatomnixima did you manage to fix this issue? I'm currently facing the exact same problem with the exact same project deps upgrade.

szuleta-thirdparties avatar Aug 02 '25 17:08 szuleta-thirdparties

@rakibatomnixima did you manage to fix this issue? I'm currently facing the exact same problem with the exact same project deps upgrade.

Hey! I wasn’t able to fix the issue directly, but I did manage to get emails working reliably by exporting to static HTML and doing simple string replacements.

Here’s how I handled it:

1. Added raw-loader to Next.js webpack config to import .html files:

// next.config.ts
webpack: (config) => {
  config.module.rules.push({
    test: /\.html$/,
    use: ['raw-loader'],
  });
  return config;
};

2. Used string {{placeholders}} inside the templates and then exported them to HTML

<Text>Hi {userFirstname || '{{userFirstname}}'},</Text>
<Button href={verificationLink || '{{verificationLink}}'}>Verify</Button>

3. Wrote a renderEmail function that loads the HTML and replaces placeholders dynamically:

import emailVerify from '../out/email/verify.html';
import { EmailVerificationEmailProps } from '../emails/email/verify';

const templatesLocationMap = {
    EmailVerification: emailVerify,
    // ... other templates
};

export type RenderEmailProps =
    | {
          template: 'EmailVerification';
          emailProps: EmailVerificationEmailProps;
      }
    // ... other templates

export async function renderEmail({ template, emailProps }: RenderEmailProps): Promise<string> {
    const html = templatesLocationMap[template];
    return html.replace(/{{(.*?)}}/g, (match: string, p1: keyof RenderEmailProps['emailProps']) => emailProps[p1]);
}

4. Used the renderEmail function in my actual sendEmail function like this (using SES, but should work with others too):

type SendEmailProps = {
    email: string;
    subject: string;
    renderEmailProps: RenderEmailProps;
};

export async function sendEmail({ email, subject, renderEmailProps }: SendEmailProps): Promise<{ success: boolean; message: string }> {
    try {
        const content = await renderEmail(renderEmailProps);
        const params: SendEmailCommandInput = {
            Source: `${appName} Team <${transactionalSource}>`,
            Destination: {
                ToAddresses: [email],
            },
            Message: {
                Body: { Html: { Charset: 'UTF-8', Data: content } },
                Subject: { Charset: 'UTF-8', Data: subject },
            },
        };
        const response = await ses.sendEmail(params);

        if (!response.MessageId) {
            throw new Error('Failed to send email.');
        }

        return { success: true, message: 'Email sent successfully.' };
    } catch (error) {
        console.error('Error sending email:', error);
        return { success: false, message: 'Failed to send email.' };
    }
}

This way, you can still build and preview your emails using react-email, but bypass runtime rendering issues.

Hope this helps!

rakibatomnixima avatar Aug 03 '25 06:08 rakibatomnixima

Thanks, pal @rakibatomnixima! Much appreciated!

szuleta-thirdparties avatar Aug 03 '25 17:08 szuleta-thirdparties

Think this might well be resolved now in Cloudflare Workers as the MessageChannel API is now available:

https://x.com/_ashleypeacock/status/1955647915663282532

For reference, a solution that worked for me (when building with esbuild, I would assume there are comparable options for other tools) was to use this in the configuration:

alias: { 'react-dom/server.browser': 'react-dom/server.edge' },

philipatkinson avatar Aug 13 '25 15:08 philipatkinson

Should be partially fixed in @react-email/[email protected]/@react-email/[email protected], but another issue happens when running it in Next.js on the edge, although it's another error.

If anyone still experiences this, let me know with a reproduction and I'll reopen.

gabrielmfern avatar Aug 20 '25 15:08 gabrielmfern

@gabrielmfern some folks here using the Convex runtime are still experiencing this - would you be open to a pr adding a convex exports entry?

erquhart avatar Aug 23 '25 20:08 erquhart

@erquhart Yes, would love if you could open it

But I wonder why Convex doesn't use the edge export itself?

gabrielmfern avatar Sep 05 '25 12:09 gabrielmfern