Search code examples
javascriptreactjsexpressserver-side-renderinghydration

Troubleshooting React issue: warning 'Expected server HTML to contain a matching <header> in <div>' with server-side rendering and Express


I've been trying to set up SSR with react for my personal project and have been running into some issues. The console is throwing a bunch of errors, the first one of which is the warning from the title, and then it defaults to CSR. I am not using NextJS, just express, ejs, node, and react. My setup is as follows:

My server.jsx file, which handles the GET request, renders the component to html, inserts it into my html with ejs, and sends it to the client:

app.get("/campus/:id/locations", async (req, res) => {
  const reactComponent = renderToString(<SchoolPage />);
  const filePath = path.join(__dirname, "dist", "school-page.ejs");
  ejs.renderFile(filePath, { reactComponent }, (err, html) => {
    if (err) {
      console.error("Error rendering template:", err);
      return res.status(500).end();
    }
    res.send(html);
  });
});

My component itself:

export default function SchoolPage() {
  return (
    <>
      <header>
        <picture title="Campus Eats">
          <source
            media="(min-width: 400px)"
            srcSet="/images/campus-eats-logo-black.svg"
          />
          <img src="/images/campus-eats-logo-mini.svg" alt="campus-eats-logo" />
        </picture>
        <nav className="places-at">
          <h2>Places</h2>
          <h2>at</h2>
          <div className="search-container">
            <MiniSearchBar></MiniSearchBar>
          </div>
        </nav>
        <nav className="login-signup">
          <button className="login">Log in</button>
          <button className="signup">Sign up</button>
        </nav>
      </header>
      <section>
        <ContentContainer></ContentContainer>
      </section>
    </>
  );
}

The ejs file it is injected into:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link
      href="https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="/styles/reset.css" />
    <link rel="stylesheet" href="/styles/school-page.css" />
    <script src="/school-page.js" defer></script>
    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
    <link rel="manifest" href="/site.webmanifest" />
    <meta name="msapplication-TileColor" content="#da532c" />
    <meta name="theme-color" content="#ffffff" />
    <title>Document</title>
  </head>
  <body>
    <div class="root">
      <!-- Rendered React component will be injected here -->
      <%- reactComponent %>
    </div>
  </body>
</html>

I have heard this sort of error comes from malformed HTML, but there seems to be little documentation on what sort of structure could cause this. I have heard issues with tables and nesting elements inside <p>, but nothing about headings. I have run the html generated by the server through several html validators and nothing seems to be wrong with it. I've even tried deleting portions of the component, like the header, but this will just yield a similar error with the next nested component ( Expected server HTML to contain a matching <section> in <div>). For reference, here's the html that the server is sending to the frontend:

<!DOCTYPE html>
<html lang="en">
   <head>
      <meta charset="UTF-8" />
      <meta http-equiv="X-UA-Compatible" content="IE=edge" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <link
         href="https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
         rel="stylesheet"
         />
      <link rel="stylesheet" href="/styles/reset.css" />
      <link rel="stylesheet" href="/styles/school-page.css" />
      <script src="/school-page.js" defer></script>
      <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
      <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
      <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
      <link rel="manifest" href="/site.webmanifest" />
      <meta name="msapplication-TileColor" content="#da532c" />
      <meta name="theme-color" content="#ffffff" />
      <title>Document</title>
   </head>
   <body>
      <div class="root">
         <!-- Rendered React component will be injected here -->
         <header>
            <picture title="Campus Eats">
               <source media="(min-width: 400px)" srcSet="/images/campus-eats-logo-black.svg"/>
               <img src="/images/campus-eats-logo-mini.svg" alt="campus-eats-logo"/>
            </picture>
            <nav class="places-at">
               <h2>Places</h2>
               <h2>at</h2>
               <div class="search-container">
                  <input type="search" placeholder="Find my school!" value=""/>
                  <div class="suggestions">
                     <ul></ul>
                  </div>
               </div>
            </nav>
            <nav class="login-signup"><button class="login">Log in</button><button class="signup">Sign up</button></nav>
         </header>
         <section>
            <div class="locations"></div>
         </section>
      </div>
   </body>
</html>

I've also heard that if the frontend does some operation to alter the page that it could result in a mismatch. My search bar component has a suggestions feature which fetches data and displays it, however I can't see this being the issue because when it initially loads it should be the same. Additionally, deleting the component entirely doesn't solve the problem either.


Solution

  • I doubt anyone will see this, but if anyone else is running into this issue I did two things to solve this problem. For one, I doubt this is important, but I changed <div class="root"> to <div id="root">. What was more important, and probably causing this issue was that React injects the component inline, meaning that if you try and write your html template like so:

    <div class="root">
      <!-- Rendered React component will be injected here -->
      <%- reactComponent %>
    </div>
    

    React will scream at you because it was not expecting the component to be injected on a new line from the root div. So you have to write it like so:

    <div class="root"><%- reactComponent %></div>
    

    A simple fix but also an easy one to overlook.