resend / react-email

💌 Build and send emails using React
https://react.email
MIT License
13.3k stars 600 forks source link

Nextjs 13 & 14 app router bug #871

Open maccman opened 12 months ago

maccman commented 12 months ago

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: 'alex@example.com',
  })

  return new Response('Hello world!')
}

Expected Behavior

Should work!

What's your node version? (if relevant)

No response

Frumba commented 11 months ago

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

maccman commented 11 months ago

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

cusxio commented 10 months ago

Could you provide an example on how you patch it?

Pety99 commented 10 months ago

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

matannahmani commented 10 months ago

any solutions i also have just encountered this issue

bramvdpluijm commented 10 months ago

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

adidoes commented 10 months ago

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

voinik commented 9 months ago

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 commented 8 months ago

This SO answer worked for me

gabrielmfern commented 6 months ago

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 ```patch 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; +declare const render: (component: React.ReactElement, options?: Options) => Promise; 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; +declare const render: (component: React.ReactElement, options?: Options) => Promise; 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 = ''; - 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 = ''; - 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.

rene-demonsters commented 2 months ago

@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