.renderToString() &-.hydrate()

After loading the server-rendered HTML for a node, the client should 'hydrate' the node to preserve it and only attach event handlers to the node (hydrateRoot() works like createRoot() followed by render(), but reuses the server-rendered markup instead of re-creating it).


// MyProject/public/components/App.jsimport React from "react";function Greetings(props){
  return (<h2>{props.msg}</h2>);}function App(props) {
  return (<Greetings msg={props.msg}/>);}export default App;

// MyProject/public/home.jsimport React from "react";import { hydrateRoot } from "react-dom/client";import App from "./components/App";hydrateRoot(
  document.getElementById("root"),
  <App msg="hi" />);

// MyProject/server.jsimport express from "express";import React from "react";import ReactDOMServer from "react-dom/server";import App from "./public/components/App.js";var app = express();app.use(express.static("./dist/public"));app.get('/', (req,res)=>{
  const component = ReactDOMServer.renderToString(<App msg="hi" />);
  const html = `
  <!DOCTYPE html><html>
    <head></head>
    <body>
      <div id="root">${component}</div>
      <script src="/home.js" type="module"></script>
    </body>
  </html>`;
  res.send(html);}); app.listen(4000);console.log('Running an Express server at http://localhost:4000/');

If you inspect the DOM on the Developer's Tools Interface (F12), you will find a few extra hydration marker comments React uses internally to line up the server-rendered markup with the client render.

If you don't need hydration at all — for example, you're generating fully static markup with no client-side interactivity — use ReactDOMServer.renderToStaticMarkup() instead of

ReactDOMServer.renderToString() when rendering the markup on the server side.