Nextjs 13 & 14 app router bug
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
Hey ! I am having exactly the same issue when using edge/runtime :/
I've also found that <Tailwind /> doesn't work under the edge env.
Could you provide an example on how you patch it?
@maccman Could you please share how you pathed it? 🙏
any solutions i also have just encountered this issue
I'm also encountering this issue. @maccman could you post your solution?
I just ran into this too, was there a patch posted anywhere?
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.
This SO answer worked for me
Here's a workaround for now for anyone that's still hitting this issue up:
-
Upgrade your
@react-email/render,@react-email/tailwind, and@react-email/components(where applicable) to the latest because@react-email/render's latest has therenderAsyncbest tuned for the latest React and performance and@react-email/tailwindremoves its use ofrenderToStaticMarkupon the latest. -
Apply this patch that completely replaces
renderwithrenderAsyncdiff --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 }; -
Replace all occurrences of
renderAsyncwith justrender
This should work for Next 13 and 14 alike, something that serverComponentsExternalPackages did not.
@gabrielmfern Your answer also works if you get this error:
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