remarkablemark/html-react-parser

Next.js 15 Circular Structure Error When passing `domNode` to another server component

Opened this issue · 17 comments

I'm not sure if this is a bug, or some problem with my NextJS environment, but in Next 14, I could pass domNode from within the replace() function to other server components, and keep my react customizations more organized. But after upgrading to Next 15, I'm getting the following error:

[ Server ] Error: Converting circular structure to JSON
    --> starting at object with constructor 'Element'
    |     property 'prev' -> object with constructor 'Element'
    --- property 'next' closes the circle

Is this a bug?

  const options: HTMLReactParserOptions = {
    replace(domNode) {
      if (domNode instanceof Element && domNode.attribs) {
        if (domNode.name === 'ul') {
          return <CustomList node={domNode} />;
				}
			}
		}
	}

Can you provide a reproducible example @webplantmedia? Is this happening on the server-side or client-side? Just curious: if you render with 'use client', does the same error appear?

Happening all on the server side. I read in HTML from an API. I parse the content like so:

  const html = parse(content, options);

My options are defined similar to the code here:

const options: HTMLReactParserOptions = {
    replace(domNode) {
      if (domNode instanceof Element && domNode.attribs) {
        if (domNode.name === 'ul') {
          return <CustomList node={domNode} />;
				}
			}
		}
	}

Next 14, no error. Next 15, Circular Structure Error.

Gotcha I'll try to reproduce this in the example Next.js app in this repository when I have some time today

I upgraded the example Next.js app to v15 and I wasn't able to reproduce your error: https://github.com/remarkablemark/html-react-parser/tree/master/examples/nextjs

Let me know if there's anything I can adjust to reproduce your error

My suspicion is that it's coming from this line:

<CustomList node={domNode} />

Do you call JSON.stringify on domNode in <CustomList>?

Even this is producing the error for me:

  const options: HTMLReactParserOptions = {
    replace(domNode) {
      function test(node: any) {
        console.log(node);
      }
      test(domNode);
    },
  };
  const html = parse(content, options);

  return <>{html}</>;

Error output:

Error: Converting circular structure to JSON
    --> starting at object with constructor 'Element'
    |     property 'next' -> object with constructor 'Element'
    --- property 'prev' closes the circle
    at test (rsc://React/Server/webpack-internal:///(rsc)/./components/blocks/parse-blocks.tsx?2:17:25)
    at Object.replace (rsc://React/Server/webpack-internal:///(rsc)/./components/blocks/parse-blocks.tsx?3:19:13)
    at ParseBlocks (rsc://React/Server/webpack-internal:///(rsc)/./components/blocks/parse-blocks.tsx?4:22:79)
    at resolveErrorDev (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js:1792:63)
    at processFullStringRow (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js:2071:17)
    at processFullBinaryRow (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js:2059:7)
    at progress (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js:2262:17)
image

Do you get the same error if you do this in the example Next.js app? https://github.com/remarkablemark/html-react-parser/tree/master/examples/nextjs

Yes, I do get the same error when in the app directory. No error if in the pages directory. Here is my code in the app directory.

layout.tsx

export const metadata = {
  title: 'Next.js',
  description: 'Generated by Next.js',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

page.tsx

import parse, { Element } from 'html-react-parser';

type Props = {
  params: { slug: string };
};

export default async function Page({ params }: Props) {
  return (
    <main>
      <h1 className="title">
        {parse(
          `
            Welcome to <a href="https://nextjs.org">Next.js</a>
            and HTMLReactParser!
          `,
          {
            replace(domNode) {
              function test(node: any) {
                console.log(node);
              }
              test(domNode);

              if (domNode instanceof Element && domNode.name === 'a') {
                return (
                  <a href="https://nextjs.org" rel="noopener noreferrer">
                    Next.js
                  </a>
                );
              }
            },
          }
        )}
      </h1>
    </main>
  );
}

Error:

Error: Converting circular structure to JSON
    --> starting at object with constructor 'Text'
    |     property 'next' -> object with constructor 'Element'
    --- property 'prev' closes the circle
    at test (rsc://React/Server/webpack-internal:///(rsc)/./app/page.tsx?0:20:33)
    at Object.replace (rsc://React/Server/webpack-internal:///(rsc)/./app/page.tsx?1:22:21)
    at Page (rsc://React/Server/webpack-internal:///(rsc)/./app/page.tsx?2:14:84)
    at resolveErrorDev (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js:1792:63)
    at processFullStringRow (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js:2071:17)
    at processFullBinaryRow (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js:2059:7)
    at progress (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js:2262:17)
image

package.json

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "html-react-parser": "^5.1.18",
    "next": "^15.0.2",
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@types/node": "22.8.6",
    "@types/react": "18.3.12",
    "typescript": "5.6.3"
  }
}

Can you fork and create a branch with these changes? @webplantmedia

https://github.com/webplantmedia/html-react-parser

I just pushed a commit with the code. Thanks so much for looking into it! I have based a very big next js project in using this react parser. I'm hoping there is an easy fix to this without having to refactor lots of code.

https://github.com/webplantmedia/html-react-parser/tree/master/examples/nextjs

Thanks! I'll take a look later today @webplantmedia

Any insight on the nature of this error? Is it some new code procedure introduced in next js 15? If it is permanent, would I need to drop support of this parser if I need to stick with Next JS? Thank you again. I've been stuck on this for two days.

@webplantmedia this is happening because Next.js tries to render the data as JSON in a <script> tag:

Screenshot 2024-11-01 at 4 53 07 PM

There are 3 solutions:

  1. Add 'use client' to the top of the file <CustomList> but you will lose server-side rendering
  2. Instead of passing domNode to the function, pass the specific keys and values
  3. Remove circular references from domNode: https://stackoverflow.com/questions/66091604/how-do-i-remove-all-circular-references-from-an-object-in-javascript

Sigh. I really needed to be able to loop inside other components. And keep it as a server component. Do you see this being a permanent move by nextjs 15 on how they handle complex objects?

I really appreciate your insight. Thanks so much.

@webplantmedia I'm not sure, but I think it's worth creating an issue or discussion on Next.js to confirm. For now, I think 3 could resolve the issue for you, but there may be a chance that it breaks your project.

I did!

vercel/next.js#72189

I'm assuming if I remove circular reference, it would break domToReact(node?.children as DOMNode[], options) in other components. Let me know if that's not the case. Also, seems like a lot of extra recursion too for large html content.

But I can't even console log domNode if I want to debug in a server component.

Two other html parsers I looked into also have circular referencing.

Sigh.

@webplantmedia yes there's a possibility that if you remove circular reference that it would break domToReact(). This library is built using html-dom-parser, which uses htmlparser2 under the hood. That's where the circular references are coming from.