Resolving React hydration issues related to third-party scripts in HTML content from a rich text editor

Issue Overview

I’m utilizing a content management system where editors are allowed to input custom JavaScript within a rich text editor. The content may consist of external scripts and CSS styles that must function correctly on the frontend.

Example of editor content:

<script>
    window.config = 'dashboard';
</script>
<script defer src="/assets/js/vendor.js?v=2"></script>
<script defer src="/assets/js/main.js?v=2"></script>
<link href="/assets/css/styles.css?v=2" rel="stylesheet">

Implementation in React Component

I have developed a component that should render this content by leveraging a custom parser:

import { graphql } from "gatsby";
import React from "react";
import { Container, Row, Col } from "react-bootstrap";
import ContentParser from "../../components/ContentParser";
import PageHead from "../../components/PageHead";

const ContentPage = ({ pageData }) => {
  return (
    <Container className="content-wrapper">
      <Row>
        <Col>
          <ContentParser
            htmlContent={pageData?.contentNode?.body?.processed}
          />
        </Col>
      </Row>
    </Container>
  );
};

export default ContentPage;

export const query = graphql`
  query ($slug: String!) {
    contentNode(id: { eq: $slug }) {
      title
      body {
        processed
      }
      seo {
        metaDescription
        metaKeywords
      }
      url {
        path
      }
      featured {
        featuredImage {
          src
        }
      }
    }
  }
`;

ContentParser component:

import React from "react";
import parseHtml from "html-react-parser";

const ContentParser = ({ htmlContent, customClasses }) => {
  const processedContent = parseHtmlContent(htmlContent);
  return (
    <div className={`parsed-content ${customClasses || ''}`}>
      {processedContent}
    </div>
  );
};

function parseHtmlContent(content) {
  return parseHtml(content, {
    replace: (element) => {
      if (element.name === 'img') {
        return (
          <img
            src={element.attribs.src}
            alt={element.attribs.alt || 'Content image'}
            className="responsive-image"
            loading="lazy"
          />
        );
      }
      return element;
    }
  });
}

export default ContentParser;

The Problem

After transitioning from dangerouslySetInnerHTML to the custom parser, I encountered hydration errors like:

Text content does not match the server-rendered HTML
Hydration failed due to mismatch between server-rendered UI and client-rendered UI
There was an error during hydration

The external JavaScript is not executing anymore, causing third-party widgets to fail to display. What steps can I take to address these hydration issues while correctly parsing and rendering rich text content that includes embedded scripts?

I’ve hit this same hydration issue when moving away from dangerouslySetInnerHTML. Your server renders one thing, but React expects something else on the client. Since html-react-parser skips script execution, you’re missing the dynamic content those scripts create. Try a two-phase approach: render a placeholder during hydration, then swap it with parsed content using useLayoutEffect after hydration finishes. You can slap suppressHydrationWarning={true} on your container div to quiet the warnings while you fix this. For the JavaScript bits, manually pull out script tags from your HTML and run them after the DOM elements load. This gets third-party widgets working without breaking hydration.

i totally relate! had similar probs with third-party scripts in gatsby. yeah, html-react-parser strips scripts so u need to run them after the component mounts. use a ref to append those scripts manually. also, check for any discrepancies in server and client HTML. good luck!

Interesting setup! Are you running the script manually after the component mounts? html-react-parser strips script tags by default for security. You could try a useEffect hook to inject the scripts after hydration finishes. What’s showing up in your browser console when the hydration mismatch happens?