back

React - Server-side Rendering (SSR)

code snippet

A sample from a DEV Community post 1.

# init
npm init

# https://koajs.com/
npm install @koa/router@12.0.0
npm install koa@2.14.2
npm install koa-bodyparser@4.4.1

# https://react.dev/
npm install react@18.2.0
npm install react-dom@18.2.0

# https://babeljs.io/docs/babel-cli
# https://babeljs.io/docs/babel-register
# https://www.npmjs.com/package/babel-plugin-transform-react-jsx
npm install --save-dev @babel/cli@7.22.10
npm install --save-dev babel-plugin-transform-react-jsx@6.24.1
npm install --save-dev @babel/register@7.22.5
.babelrc
{
  "plugins": ["transform-react-jsx"]
}

app.js
const React = require('react');
const App = () => {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <title>React SSR</title>
      </head>
      <body>
        <div id="root">
          <h1>Hello, world!</h1>
        </div>
      </body>
    </html>
  );
}

module.exports = App

client.js
const React = require('react');

const { hydrateRoot } = require('react-dom/client');

const App = require('./app');

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

router-ssr.js
const React = require('react');
const { renderToPipeableStream } = require('react-dom/server');
const App = require('./app');

const router = async (ctx) => {
  let didError = false;
  try {
    // Wraps into a promise to force Koa to wait for the render to finish
    return new Promise((_resolve, reject) => {
      const { pipe, abort } = renderToPipeableStream(
        <App />,
        {
          bootstrapModules: ['./client.js'],
          onShellReady() {
            ctx.respond = false;
            ctx.status = didError ? 500 : 200;
            ctx.set('Content-Type', 'text/html');
            pipe(ctx.res);
            ctx.res.end();
          },
          onShellError() {
            ctx.status = 500;
            abort();
            didError = true;
            ctx.set('Content-Type', 'text/html');
            ctx.body = '<!doctype html><p>Loading...</p><script src="clientrender.js"></script>';
            reject();
          },
          onError(error) {
            didError = true;
            console.error(error);
            reject();
          }
        },
      );

      setTimeout(() => {
        abort();
      }, 10_000);
    })
  } catch (err) {
    console.log(err);
    ctx.status = 500;
    ctx.body = 'Internal Server Error';
  }
};

module.exports = router

server.js
const Router = require('@koa/router');
const Koa = require('koa');
const bodyparser = require('koa-bodyparser');
const ssr = require('./router-ssr')

const router = new Router();
const app = new Koa();

router.get('/app', ssr);

router.get('/(.*)', async (ctx) => {
  ctx.status = 200;
  ctx.body = 'OK';
});

app.use(bodyparser());
app.use(router.routes());
app.use(router.allowedMethods());

module.exports = app;

index.js
require("@babel/register");

const http = require('http');
const _server = require('./server');

const currentHandler = _server.callback();
const server = http.createServer(_server.callback());

server.listen(4001, (error) => {
  console.log('listening...');
  console.error(error);
});

After creating all files, the repo should look like below.

 |
 |-/.babelrc
 |-/app.js
 |-/client.js
 |-/router-ssr.js
 |-/server.js
 |-/index.js
 |-/package-lock.json
 |-/package.json
# run the server
node index.js
# GET request
curl localhost:4001/app