next.js icon indicating copy to clipboard operation
next.js copied to clipboard

Unable to import react-dom/server in a server component

Open janus-reith opened this issue 3 years ago • 16 comments

Verify canary release

  • [X] I verified that the issue exists in the latest Next.js canary release

Provide environment information

Operating System: Platform: darwin Arch: arm64 Version: Darwin Kernel Version 22.1.0: Sun Oct 9 20:14:30 PDT 2022; root:xnu-8792.41.9~2/RELEASE_ARM64_T8103 Binaries: Node: 16.18.0 npm: 8.19.2 Yarn: 1.22.19 pnpm: 7.13.6 Relevant packages: next: 13.0.7-canary.1 eslint-config-next: N/A react: 18.2.0 react-dom: 18.2.0

Which area(s) of Next.js are affected? (leave empty if unsure)

App directory (appDir: true)

Link to the code that reproduces this issue

https://stackblitz.com/edit/vercel-next-js-arwqsz?file=app%2Fpage.js

To Reproduce

app/page.js:

import ReactDOMServer from "react-dom/server";

const staticMarkup = ReactDOMServer.renderToStaticMarkup(<p>TEST</p>);

export default function TestPage() {
  return <p>123</p>;
}

Run next dev and try opening the index page.

Describe the Bug

I'm receiving an error message:

Failed to compile. ./app/page.js

You're importing a component that imports react-dom/server. To fix it, render or return the content directly as a Server Component instead for perf and security.

Maybe one of these should be marked as a client entry "use client": app/page.js

Expected Behavior

Be able to use react-dom/server on the server. My usecase it to take some React tree, render it so markup, run it through another postprocessing step which is not aware of anything React specific, and then, return an iframe containing that html.

Contrary to what the error message says, I think there's nothing to fix about importing react-dom/server, and neither should any of this require using the "use client" directive

Which browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

No response

janus-reith avatar Dec 07 '22 12:12 janus-reith

It's similar to what https://github.com/FormidableLabs/react-ssr-prepass and others like https://github.com/kmoskwiak/useSSE rely on (e.g. a real or simulated pre-render pass to hoist certain work and then actually send it already digested into SSR as initial props)

In general, this kind of pattern should become even more of an edge-case, as with RSC you could fetch/do async stuff anywhere and Next.js would attempt to deduplicate the requests.

However, if you do need to render something static (as your example shows, but not pre-rendering your own app itself), you might be able to work around it by dynamically importing ReactDOMServer (see forked stackblitz).

// app/precompile.js
const getData = async (component) => {
  const ReactDOMServer = (await import('react-dom/server')).default;
  const staticMarkup = ReactDOMServer.renderToStaticMarkup(component);
  return staticMarkup;
};

export default getData;

// app/page.js
const STATIC_COMPONENT = <p>Static Component</p>;

export default async function TestPage() {
  // this works OK
  const prerenderStaticComponent = await getData(STATIC_COMPONENT);

  // this does not work (error)
  /**
   * Error: Objects are not valid as a React child (found: object with keys {$$typeof, filepath, name, async}). * If you meant to render a collection of children, use an array instead.
   */
  // const prerenderAClientComponent = await getData(PageWrapperClient);

  // this does not work (warning)
  /**
   * Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.
   */
  // const prerenderAServerComponent = await getData(PageWrapperServer);

  return (
    <PageWrapperClient>
      <h3>This is a page with static markup</h3>
      <div dangerouslySetInnerHTML={{ __html: prerenderStaticComponent }} />
    </PageWrapperClient>
  );
}

But trying to render your actual tree of components will likely fail as they'll be client or server components and both cases will error out (not quite sure how Next.js internally is handling this to be able to render these new trees, if there's any webpack work involved and why it doesn't just work on its own)

juanferreras avatar Dec 07 '22 15:12 juanferreras

Same problem here. My use case is rendering a page with various components into a static RSS feed using ReactDOMServer.renderToStaticMarkup.

zoul avatar Mar 09 '23 10:03 zoul

I found this while trying to render a react component svg in a route.tsxhandler:

export async function GET(req: NextRequest): Promise<NextResponse> {
  const ReactDOMServer = (await import('react-dom/server')).default
  const component = <ReactComponent />
  const svg = Buffer.from(ReactDOMServer.renderToString(component))
  const png = await sharp(svg).flatten({ background: 'white' }).png().toBuffer()

  return new NextResponse(png, {
    status: 200,
    headers: { 'Content-Type': 'image/png' },
  })
}

Importing ReactDOMServer dynamically does fix this. So thank you – just wanted to share my usecase :-)

errnesto avatar Mar 09 '23 13:03 errnesto

Thanks, dynamic import worked for me! My use case is to convert mdx to text to prepare my content for meilisearch, I think that use case is legitimate. Had to do something like this:

export async function toMeilisearch(markdown: string | undefined) {
	const { renderToString } = await import('react-dom/server')

	// from @mdx-js/mdx
	const content = await _evaluate(markdown ?? '', {
		...runtime,
		Fragment,
		remarkPlugins: [remarkGfm, remarkFrontmatter],
		development: false,
	})

	const rendered = renderToString(createElement(content.default))
	
	// from html-to-text
	return convert(rendered, {
		selectors: [
			{
				selector: 'a',
				options: {
					ignoreHref: true,
					uppercaseHeaderCells: false,
				},
			},
		],
	})
}

CapitaineToinon avatar Jul 06 '23 13:07 CapitaineToinon

I was able to import

import ReactDOMServer from "react-dom/server.browser";

and use it on the server

tomer-tgp avatar Aug 21 '23 20:08 tomer-tgp

Same problem building a sitemap.xml with dynamic urls imported from a database is my use case.

tlchatt avatar Oct 06 '23 21:10 tlchatt

I have the same problem

https://github.com/vercel/next.js/discussions/57631

JacobWeisenburger avatar Oct 27 '23 20:10 JacobWeisenburger

Here's my solution to render jsx to string on both client and server on NextJS > 13 without having server component issue in import

// render-client.js

import ReactDom from "next/dist/compiled/react-dom/cjs/react-dom-server-legacy.browser.production";
export function renderToStringClient(element) {
  return ReactDom.renderToString(element);
}

// render-server.ts

export function renderToStringServer(element: any) {
  const ReactDOMServer = require("react-dom/server");
  const html = ReactDOMServer.renderToString(element);

  return html;
} 

// index.tsx

import { renderToStringClient } from "./render-client";

export function renderToString(JSXElement: JSX.Element) {
  const isServer = typeof window === "undefined";
  if (isServer) {
    const { renderToStringServer } = require("./render-server");
    // do your server stuff here
    return renderToStringServer(JSXElement);
  }
  // do your client stuff here
  return renderToStringClient(JSXElement);
}

chemiadel avatar Jan 28 '24 02:01 chemiadel

chemiadel

wow

"use client";
// @ts-ignore
import ReactDom from "next/dist/compiled/react-dom/cjs/react-dom-server-legacy.browser.production";
import { SiRootme } from "react-icons/si";

export default function TestPage({}) {
  const html = ReactDom.renderToStaticMarkup(<SiRootme />);

  const json = JSON.stringify(["SiRootme", html]);

  console.log({ icon: json });
  return <></>;
}

0-don avatar Feb 14 '24 22:02 0-don

I found this while trying to render a react component svg in a route.tsxhandler:

export async function GET(req: NextRequest): Promise<NextResponse> {
  const ReactDOMServer = (await import('react-dom/server')).default
  const component = <ReactComponent />
  const svg = Buffer.from(ReactDOMServer.renderToString(component))
  const png = await sharp(svg).flatten({ background: 'white' }).png().toBuffer()

  return new NextResponse(png, {
    status: 200,
    headers: { 'Content-Type': 'image/png' },
  })
}

Importing ReactDOMServer dynamically does fix this. So thank you – just wanted to share my usecase :-)

It does not work now: fails with the following message: Error: react-dom/server is not supported in React Server Components.

abiriadev avatar May 18 '24 18:05 abiriadev

I found this while trying to render a react component svg in a route.tsxhandler:

export async function GET(req: NextRequest): Promise<NextResponse> {
  const ReactDOMServer = (await import('react-dom/server')).default
  const component = <ReactComponent />
  const svg = Buffer.from(ReactDOMServer.renderToString(component))
  const png = await sharp(svg).flatten({ background: 'white' }).png().toBuffer()

  return new NextResponse(png, {
    status: 200,
    headers: { 'Content-Type': 'image/png' },
  })
}

Importing ReactDOMServer dynamically does fix this. So thank you – just wanted to share my usecase :-)

This solution from @abiriadev worked for me.

Daisymond avatar Jun 26 '24 09:06 Daisymond

This should be the best answer as of today.

I was able to import

import ReactDOMServer from "react-dom/server.browser";

and use it on the server

adgang avatar Jul 17 '24 06:07 adgang

when I try to render a dynamic components with props it gives me this Error:

Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.

My code is like below and it runs when we call an api (so in the server side):

const ReactDOMServer = require("react-dom/server");
const element = createElement(component, data);
const html = ReactDOMServer.renderToString(element);

does anyone has a solution?

ako-v avatar Aug 11 '24 06:08 ako-v

I found this while trying to render a react component svg in a route.tsxhandler:

export async function GET(req: NextRequest): Promise<NextResponse> {
  const ReactDOMServer = (await import('react-dom/server')).default
  const component = <ReactComponent />
  const svg = Buffer.from(ReactDOMServer.renderToString(component))
  const png = await sharp(svg).flatten({ background: 'white' }).png().toBuffer()

  return new NextResponse(png, {
    status: 200,
    headers: { 'Content-Type': 'image/png' },
  })
}

Importing ReactDOMServer dynamically does fix this. So thank you – just wanted to share my usecase :-)

It works indeed, but crashes with

 ⨯ Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
    at renderElement (webpack-internal:///(rsc)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-server-legacy.node.development.js:10130:9)

If using "use client" in the tsx file.

That might feel obvious, but my need was to generate React code from an API endpoint which would return HTML, and have some React "client" run on the frontend, but I couldn't achieve that.

DNA-PC avatar Aug 11 '24 16:08 DNA-PC

when I try to render a dynamic components with props it gives me this Error:

Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.

My code is like below and it runs when we call an api (so in the server side):

const ReactDOMServer = require("react-dom/server");
const element = createElement(component, data);
const html = ReactDOMServer.renderToString(element);

does anyone has a solution?

Same Problem here. Has anyone found a solution to this issue?

Kasitphoom avatar Aug 14 '24 04:08 Kasitphoom

I don't think react-dom/server.browser is a good idea, since that's a different bundle, made specifically for the browser, and not Node.

I hacked my way around this by creating my own wrapper around react-dom/server.

utils/react-dom.ts

import type { renderToStaticMarkup as _renderToStaticMarkup } from 'react-dom/server'

export let renderToStaticMarkup: typeof _renderToStaticMarkup
import('react-dom/server').then((module) => {
  renderToStaticMarkup = module.renderToStaticMarkup
})

page.tsx

import { renderToStaticMarkup } from "@/utils/react-dom";

export default function Page() {
  return (
    <section>
      {renderToStaticMarkup(
        <h1>
          hello <strong>world</strong>
        </h1>
      )}
    </section>
  );
}

This even preserves the types and I can use renderToStaticMarkup as usual, except I have to load it from @/utils/react-dom and not react-dom/server.

The only downside I see is that there is a fraction of a second where renderToStaticMarkup would be undefined when the server starts, but I think this wouldn't realistically be a problem.

hdodov avatar Oct 25 '24 14:10 hdodov

This Solution by @hdodov worked, But I am facing another issue now with next.js to 'use client', as most of my app components use styled-components so I have to write 'use client', which will not allow than using that inside server api, Looks like next.js is making things hard now, Even ServerStyleSheet will not work on server side, Am i missing anything

import { ServerStyleSheet } from 'styled-components';

My requirement is to generate html templates from existing react components and send html for pdf file generation.

ksrikhi avatar Nov 17 '24 18:11 ksrikhi

I would greatly appreciate it if someone could help on this issue.

import ClientComponent from "./ClientComponent";
import { renderToString } from 'react-dom/server';

export default async function Page() {
  const content = {key: 'values'}

  //convert client component to HTML-string to pass to another client compoent 
  const htmlString =  renderToString(<ClientComponent content={content} />);

  //to render using dangerouslySetInnerHTML
  return <RenderComponent htmlstring={htmlString}/>;
}

//client component with some browser context

"use client";

import { Tooltip } from "@mui/material";

export default function ClientComponent() {
  return (
    <>
     <Tooltip children={} title={}></Tooltip>
    </>
  );
}

while converting the client component to HTML-string getting below error (renderToString(<ClientComponent content={content} />)) or renderToStaticMarkup

Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.

So, could some of you please suggest how to convert a client in a server page to pass that updated HTML string to render dynamically in another component using dangerouslySetInnerHTML?

thank you.

indrasenakatam avatar Nov 18 '24 16:11 indrasenakatam

I found this while trying to render a react component svg in a route.tsxhandler: export async function GET(req: NextRequest): Promise<NextResponse> { const ReactDOMServer = (await import('react-dom/server')).default const component = <ReactComponent /> const svg = Buffer.from(ReactDOMServer.renderToString(component)) const png = await sharp(svg).flatten({ background: 'white' }).png().toBuffer()

return new NextResponse(png, { status: 200, headers: { 'Content-Type': 'image/png' }, }) }

Importing ReactDOMServer dynamically does fix this. So thank you – just wanted to share my usecase :-)

It works indeed, but crashes with

 ⨯ Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
    at renderElement (webpack-internal:///(rsc)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-server-legacy.node.development.js:10130:9)

If using "use client" in the tsx file.

That might feel obvious, but my need was to generate React code from an API endpoint which would return HTML, and have some React "client" run on the frontend, but I couldn't achieve that.

wow thank you so much, I had the same problem and indeed removing 'use client' fixed it!!!

lucafaggianelli avatar Jan 29 '25 15:01 lucafaggianelli

I'm using the react-dom/server to render a component to html which is then passed to pupeteer to create a pdf. The function is only used in a http GET request handler. It would be nice to allow for react-dom/server on the serverside ( or atleast some config to enable it ).

AkshatGiri avatar Mar 14 '25 06:03 AkshatGiri

I ran into this as well with a similar use case (I want to render React components as static HTML and do post-processing on that HTML, such as removing or replacing HTML tags).

It should be noted that react-dom/server is an official, documented React API. I don't think Next.js should be prohibiting its usage.

jonathanhefner avatar Mar 16 '25 16:03 jonathanhefner

It seems like a really aggressive design decision to vendor the react and react-dom packages, especially when the vendored versions don't correspond with what is resolved in package-lock.json (npm).

In my lockfile, for example, it looks like React should resolve to 18.2.0, but Next messes around with it and it gets replaced with a canary version of 18.3.x.

I don't see why the Next team couldn't have just written polyfills for the experimental React features they need instead of having their own quasi-package management on top of the actual package manager. Or at least make their vendored version strictly a superset of React features, and not remove things which might be required by other implementations of serverside rendering. RSC is not the only valid way to render React on the server!

samwinslow avatar Apr 24 '25 16:04 samwinslow

this worked for me:

 // Create the element
  const emailComponent = React.createElement(template, params);

  const {
    renderToStaticMarkup
  } = // @ts-expect-error missing types
    (await import("next/dist/compiled/react-dom/cjs/react-dom-server-legacy.browser.production"))
      .default;

  const html = renderToStaticMarkup(emailComponent);
  return `<!DOCTYPE html>${html}`;

sandrodesouza avatar May 06 '25 16:05 sandrodesouza

Related issue: https://github.com/vercel/next.js/issues/64052

In that other issue, the user is rendering PDF on the server:

I'm using react-pdf to generate a PDF file on the server (in a Next.js API route). My PDF has some charts, for which I use react-pdf-charts. That library uses renderToStaticMarkup from react-dom/server to generate some server-side DOM that it parses and injects into the PDF.

jbmusso avatar Aug 27 '25 20:08 jbmusso

Related issue: #64052

In that other issue, the user is rendering PDF on the server:

I'm using react-pdf to generate a PDF file on the server (in a Next.js API route). My PDF has some charts, for which I use react-pdf-charts. That library uses renderToStaticMarkup from react-dom/server to generate some server-side DOM that it parses and injects into the PDF.

Nice,

This worked for me

import { createElement } from "react";

const ReactDOMServer = (await import("react-dom/server")).default;
const reactElement = createElement(MyComponent, props);
const html = ReactDOMServer.renderToStaticMarkup(reactElement);
const pdfDoc = await htmlToPDF(html);
return pdfDoc;

The htmlToPDF function uses puppeteer to generate pdf from the html.

Shoutout to @juanferreras

It's worth noting that I'm running nextjs on a full vps not serverless or vercel so mileage may vary.

AkshatGiri avatar Sep 03 '25 10:09 AkshatGiri