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

Nextjs 13 & 14 app router bug

Open maccman opened this issue 2 years ago • 11 comments

Describe the Bug

I'm getting an error "Unable to import react-dom/server in a server component" when I try to use react-email under a API route using Nextjs' edge runtime and the new app dir routing.

Following tips from this ticket I've now patched react-email to load react-dom/server dynamically which seems to be working.

Which package is affected (leave empty if unsure)

No response

Link to the code that reproduces this issue

https://github.com/vercel/next.js/issues/43810

To Reproduce

// In app/api/emails/hello-world.ts

import React from 'react'

import { createEmail } from '@/lib/resend'
import HelloWorldEmail from '@/server/emails/hello-world'
import { renderAsync } from '@react-email/render'

export const runtime = 'edge'

export async function POST(_request: Request) {
  const html = await renderAsync(React.createElement(HelloWorldEmail))

  const text = await renderAsync(HelloWorldEmail(), {
    plainText: true,
  })

  await createEmail({
    html,
    text,
    subject: 'Hello world',
    to: '[email protected]',
  })

  return new Response('Hello world!')
}

Expected Behavior

Should work!

What's your node version? (if relevant)

No response

maccman avatar Aug 02 '23 12:08 maccman

Hey ! I am having exactly the same issue when using edge/runtime :/

Frumba avatar Aug 03 '23 13:08 Frumba

I've also found that <Tailwind /> doesn't work under the edge env.

maccman avatar Aug 03 '23 14:08 maccman

Could you provide an example on how you patch it?

cusxio avatar Sep 07 '23 13:09 cusxio

@maccman Could you please share how you pathed it? 🙏

Pety99 avatar Sep 11 '23 13:09 Pety99

any solutions i also have just encountered this issue

matannahmani avatar Sep 21 '23 13:09 matannahmani

I'm also encountering this issue. @maccman could you post your solution?

bramvdpluijm avatar Sep 23 '23 20:09 bramvdpluijm

I just ran into this too, was there a patch posted anywhere?

adidoes avatar Sep 28 '23 23:09 adidoes

I ran into this back in July and made a Discord post about it (the Tailwind component breaking on edge) as well, but no response yet. I fiddled around with react-dom/server but I couldn't get it to work. I'm curious how @maccman worked around all of this.

I ended up pre-rendering the email component off of edge with some placeholders and grabbing the HTML, and then at runtime find-replacing the placeholders with the actual values. Horrible workaround, but it's the only way I can get the flow I need to work.

voinik avatar Oct 02 '23 10:10 voinik

This SO answer worked for me

fnb-software avatar Nov 04 '23 14:11 fnb-software

Here's a workaround for now for anyone that's still hitting this issue up:

  1. Upgrade your @react-email/render, @react-email/tailwind, and @react-email/components (where applicable) to the latest because @react-email/render's latest has the renderAsync best tuned for the latest React and performance and @react-email/tailwind removes its use of renderToStaticMarkup on the latest.

  2. Apply this patch that completely replaces render with renderAsync
    diff --git a/dist/index.d.mts b/dist/index.d.mts
    index 77a03d74798bf4eb14d400325f1866130bfe3256..9447c1eb8034db1178ead7876af5f2e7fe62668f 100644
    --- a/dist/index.d.mts
    +++ b/dist/index.d.mts
    @@ -15,10 +15,8 @@ type Options = {
         htmlToTextOptions?: HtmlToTextOptions;
     });
    
    -declare const render: (component: React.ReactElement, options?: Options) => string;
    -
    -declare const renderAsync: (component: React.ReactElement, options?: Options) => Promise<string>;
    +declare const render: (component: React.ReactElement, options?: Options) => Promise<string>;
    
     declare const plainTextSelectors: SelectorDefinition[];
    
    -export { Options, plainTextSelectors, render, renderAsync };
    +export { Options, plainTextSelectors, render };
    diff --git a/dist/index.d.ts b/dist/index.d.ts
    index 77a03d74798bf4eb14d400325f1866130bfe3256..9447c1eb8034db1178ead7876af5f2e7fe62668f 100644
    --- a/dist/index.d.ts
    +++ b/dist/index.d.ts
    @@ -15,10 +15,8 @@ type Options = {
         htmlToTextOptions?: HtmlToTextOptions;
     });
    
    -declare const render: (component: React.ReactElement, options?: Options) => string;
    -
    -declare const renderAsync: (component: React.ReactElement, options?: Options) => Promise<string>;
    +declare const render: (component: React.ReactElement, options?: Options) => Promise<string>;
    
     declare const plainTextSelectors: SelectorDefinition[];
    
    -export { Options, plainTextSelectors, render, renderAsync };
    +export { Options, plainTextSelectors, render };
    diff --git a/dist/index.js b/dist/index.js
    index 9708ae623da3bf3e8979e41560a32e83529a559a..d3dd53ea4f5184d0206c845c76b8941b42c491ea 100644
    --- a/dist/index.js
    +++ b/dist/index.js
    @@ -71,15 +71,10 @@ var __forAwait = (obj, it, method) => (it = obj[__knownSymbol("asyncIterator")])
     var src_exports = {};
     __export(src_exports, {
       plainTextSelectors: () => plainTextSelectors,
    -  render: () => render,
    -  renderAsync: () => renderAsync
    +  render: () => render
     });
     module.exports = __toCommonJS(src_exports);
    
    -// src/render.ts
    -var ReactDomServer = __toESM(require("react-dom/server"));
    -var import_html_to_text = require("html-to-text");
    -
     // src/utils/pretty.ts
     var import_js_beautify = __toESM(require("js-beautify"));
     var defaults = {
    @@ -103,25 +98,6 @@ var plainTextSelectors = [
       }
     ];
    
    -// src/render.ts
    -var render = (component, options) => {
    -  if (options == null ? void 0 : options.plainText) {
    -    return renderAsPlainText(component, options);
    -  }
    -  const doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
    -  const markup = ReactDomServer.renderToStaticMarkup(component);
    -  const document = `${doctype}${markup}`;
    -  if (options && options.pretty) {
    -    return pretty(document);
    -  }
    -  return document;
    -};
    -var renderAsPlainText = (component, options) => {
    -  return (0, import_html_to_text.convert)(ReactDomServer.renderToStaticMarkup(component), __spreadValues({
    -    selectors: plainTextSelectors
    -  }, (options == null ? void 0 : options.plainText) === true ? options.htmlToTextOptions : {}));
    -};
    -
     // src/render-async.ts
     var import_html_to_text2 = require("html-to-text");
     var decoder = new TextDecoder("utf-8");
    @@ -155,7 +131,7 @@ var readStream = (readableStream) => __async(void 0, null, function* () {
       }
       return result;
     });
    -var renderAsync = (component, options) => __async(void 0, null, function* () {
    +var render = (component, options) => __async(void 0, null, function* () {
       var _a;
       const reactDOMServer = (yield import("react-dom/server")).default;
       const renderToStream = (_a = reactDOMServer.renderToReadableStream) != null ? _a : reactDOMServer.renderToStaticNodeStream;
    @@ -176,6 +152,5 @@ var renderAsync = (component, options) => __async(void 0, null, function* () {
     // Annotate the CommonJS export names for ESM import in node:
     0 && (module.exports = {
       plainTextSelectors,
    -  render,
    -  renderAsync
    +  render
     });
    diff --git a/dist/index.mjs b/dist/index.mjs
    index a0927da477df4916a663d40b0157a237c4f25590..4db020502c795039908a56d1792df44e668faadd 100644
    --- a/dist/index.mjs
    +++ b/dist/index.mjs
    @@ -41,10 +41,6 @@ var __async = (__this, __arguments, generator) => {
     };
     var __forAwait = (obj, it, method) => (it = obj[__knownSymbol("asyncIterator")]) ? it.call(obj) : (obj = obj[__knownSymbol("iterator")](), it = {}, method = (key, fn) => (fn = obj[key]) && (it[key] = (arg) => new Promise((yes, no, done) => (arg = fn.call(obj, arg), done = arg.done, Promise.resolve(arg.value).then((value) => yes({ value, done }), no)))), method("next"), method("return"), it);
    
    -// src/render.ts
    -import * as ReactDomServer from "react-dom/server";
    -import { convert } from "html-to-text";
    -
     // src/utils/pretty.ts
     import jsBeautify from "js-beautify";
     var defaults = {
    @@ -68,25 +64,6 @@ var plainTextSelectors = [
       }
     ];
    
    -// src/render.ts
    -var render = (component, options) => {
    -  if (options == null ? void 0 : options.plainText) {
    -    return renderAsPlainText(component, options);
    -  }
    -  const doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
    -  const markup = ReactDomServer.renderToStaticMarkup(component);
    -  const document = `${doctype}${markup}`;
    -  if (options && options.pretty) {
    -    return pretty(document);
    -  }
    -  return document;
    -};
    -var renderAsPlainText = (component, options) => {
    -  return convert(ReactDomServer.renderToStaticMarkup(component), __spreadValues({
    -    selectors: plainTextSelectors
    -  }, (options == null ? void 0 : options.plainText) === true ? options.htmlToTextOptions : {}));
    -};
    -
     // src/render-async.ts
     import { convert as convert2 } from "html-to-text";
     var decoder = new TextDecoder("utf-8");
    @@ -120,7 +97,7 @@ var readStream = (readableStream) => __async(void 0, null, function* () {
       }
       return result;
     });
    -var renderAsync = (component, options) => __async(void 0, null, function* () {
    +var render = (component, options) => __async(void 0, null, function* () {
       var _a;
       const reactDOMServer = (yield import("react-dom/server")).default;
       const renderToStream = (_a = reactDOMServer.renderToReadableStream) != null ? _a : reactDOMServer.renderToStaticNodeStream;
    @@ -140,6 +117,5 @@ var renderAsync = (component, options) => __async(void 0, null, function* () {
     });
     export {
       plainTextSelectors,
    -  render,
    -  renderAsync
    +  render
     };
    
  3. Replace all occurrences of renderAsync with just render

This should work for Next 13 and 14 alike, something that serverComponentsExternalPackages did not.

gabrielmfern avatar Jan 27 '24 12:01 gabrielmfern

@gabrielmfern Your answer also works if you get this error:

image

TypeError: (0 , _react_email_render__WEBPACK_IMPORTED_MODULE_4__.renderAsync) is not a function

 at renderEmailByPath (webpack-internal:///(rsc)/./src/actions/render-email-by-path.tsx:35:94)
 at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
 at async Page (webpack-internal:///(rsc)/./src/app/preview/[...slug]/page.tsx:37:34)

I had outdated versions of "@react-email/render" ("^0.0.7"), updated to 0.0.13 and "@react-email/components" ("0.0.14"), updated to 0.0.17

rene-demonsters avatar May 08 '24 10:05 rene-demonsters