React 18: Streaming SSR with Suspense and data fetching on the server (How to)
Tags: react, ssr, streaming, suspense
Published: 2022-05-15 17:51:04

## The problem

When you try to do data fetching on the server with streaming SSR with Suspense in React 18 you face a problem, and it is the hydration mismatch. Here we will explain a way to solve it (solution extracted from [here](https://github.com/XiNiHa/streaming-ssr-showcase)).

## The solution

Here is the code of the server app:

```javascript
import express from "express";
import { renderToPipeableStream } from "react-dom/server";
import React from "react";
import AppServer from "../src/components/AppServer";
import path from "path";
import { DataProvider, data } from "../src/providers/data";
import { createServerData } from "../src/api/resource";
import { Writable } from "node:stream";

const app = express();
const port = 3000;
app.get("/", (req, res) => {
  const stream = new Writable({
    write(chunk, _encoding, cb) {
      res.write(chunk, cb);
    },
    final() {
      res.write(
        `<script>
        window.globalCache={comments:[${data.comments.map((c) => `'${c}'`)}]}
        </script>`
      );
      res.end("</body></html>");
    },
  });
  const { pipe } = renderToPipeableStream(
    <DataProvider data={createServerData()}>
      <AppServer />
    </DataProvider>,
    {
      bootstrapScripts: ["/main.js"],
      onShellReady() {
        res.write("<html><body>");
        pipe(stream);
      },
    }
  );
});

app.use(express.static(path.join(__dirname, "/../dist")));

app.listen(port, () => {
  console.log(`app running on port ${port}`);
});
```

The key point is in here:

```javascript
 const stream = new Writable({
    write(chunk, _encoding, cb) {
      res.write(chunk, cb);
    },
    final() {
      res.write(
        `<script>
        window.globalCache={comments:[${data.comments.map((c) => `'${c}'`)}]}
        </script>`
      );
      res.end("</body></html>");
    },
  });
```

We are writing a script at the end of the streaming to populate the `globalCache` variable in the browser with data on the server.

This is where `data` comes from:

```javascript
import React, { createContext, useContext } from "react";

export let data;

const DataContext = createContext(null);

export function DataProvider({ children, data }) {
  return <DataContext.Provider value={data}>{children}</DataContext.Provider>;
}

export function useData() {
  const ctx = useContext(DataContext);
  if (ctx) {
    data = ctx.read();
  } else {
    data = window.globalCache;
  }
  return data;
}
```

On the server `data` is read from the context while on the browser it is read from the `globalCache` variable. That's how we avoid the hydration mismatch problem.

Let's see at the `createServerData` function:

```javascript
export function createServerData() {
    let done = false;
    let promise = null;
    let value
    return {
      read: ()=> {
        if (done) {
          return value
        }
        if (promise) {
          throw promise;
        }
        promise = new Promise((resolve) => {
          setTimeout(() => {
            done = true;
            promise = null;
            value={comments:['a','b','c']}
            resolve()
          }, 6000);
        });
        throw promise;
      }
    };
  }
```

It's a promise that resolves in `6000` ms.

Now let's look at where we use the `useData` hook, in the `Comments` component:

```javascript
import React from "react";
import { useData } from "../providers/data";

export default function Comments() {
  const { comments } = useData();

  return (
    <ul>
      {comments && comments.map((comment, i) => <li key={i}>{comment}</li>)}
    </ul>
  );
}
```

In the server it will read `data` from the `Context` while in the browser it will read `data` from the global variable `globalCache`. This is because in the browser the context will be undefined, that is because in the case of the browser we are not wrapping the `App` component with the `DataProvider`:

```javascript
import React from "react";
import { hydrateRoot } from "react-dom/client";
import App from "./components/App";

hydrateRoot(document.getElementById("root"), <App />);
```

This is how the `App` component looks like:

```javascript
import React, { Suspense, lazy } from "react";

const Comments = lazy(() => import("./Comments"));

const App = () => (
  <>
    <Suspense fallback={<div>loading...</div>}>
      <Comments />
    </Suspense>
  </>
);

export default App;
```

And here how the `AppServer` component, used above (in the server), looks like:

```javascript
import React from "react";
import App from "./App";

const AppServer = () => (
      <div id="root"> 
        <App />
       </div> 
);

export default AppServer;
```

With that we have seen all the code of this example on how to do streaming SSR with Suspense and data fetching on the server in React 18 avoiding the problem of hydration mismatch.