Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nextjs 13 & 14 app router bug #871

Open
maccman opened this issue Aug 2, 2023 · 11 comments
Open

Nextjs 13 & 14 app router bug #871

maccman opened this issue Aug 2, 2023 · 11 comments

Comments

@maccman
Copy link

maccman commented Aug 2, 2023

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

vercel/next.js#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: 'alex@example.com',
  })

  return new Response('Hello world!')
}

Expected Behavior

Should work!

What's your node version? (if relevant)

No response

@maccman maccman added the Type: Bug Confirmed bug label Aug 2, 2023
@Frumba
Copy link

Frumba commented Aug 3, 2023

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

@maccman
Copy link
Author

maccman commented Aug 3, 2023

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

@cusxio
Copy link

cusxio commented Sep 7, 2023

Could you provide an example on how you patch it?

@Pety99
Copy link

Pety99 commented Sep 11, 2023

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

@matannahmani
Copy link

any solutions i also have just encountered this issue

@bramvdpluijm
Copy link

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

@adidoes
Copy link

adidoes commented Sep 28, 2023

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

@voinik
Copy link

voinik commented Oct 2, 2023

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.

@fnb-software
Copy link

This SO answer worked for me

@gabrielmfern
Copy link
Collaborator

gabrielmfern commented Jan 27, 2024

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 gabrielmfern changed the title Nextjs 13 app router bug Nextjs 13 & 14 app router bug Jan 27, 2024
@rene-demonsters
Copy link

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests