Server-side Rendering (SSR) From Scratch with React

noghartt

Guilherme Ananias

Posted on August 30, 2023

Server-side Rendering (SSR) From Scratch with React

A core feature at Woovi is our payment link. It's an easy way to sell anywhere on the web you don't need a website or anything else. Just send a link and get paid.

Why Use Server-side Rendering?

How can we ensure a good user experience sharing this link through the web? I would like to display the QR code related to that charge and, when sending it anywhere on the web, it should display the OGs and metatags related to that HTML page.

You don't have it with a client-side rendered page because you can't store these references until access the page.

This is the reason why we opted for SSR over CSR for our payment link. We would like to ensure that, in any place on the web, if you share the link, it should display: the right title of the payment link, the QR code as the OG image, and any other dynamic metadata.

An example of OG tag in WhatsApp

Starting Server

The initial step is to create our entry point, from where the page will be rendered. In this case, we will use the koa framework.

// index.ts
import Router from '@koa/router';
import Koa from 'koa';
import bodyparser from 'koa-bodyparser';

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

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

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

export default app;
Enter fullscreen mode Exit fullscreen mode

What we're doing here is: creating a new endpoint that catches all requests and returns a 200 with an OK body. If the user hits the / or /foo/bar, it will get the same response.

Now, to run the server and open a port to access the server, you can run the code below:

// index.ts
import http from 'http';

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

server.listen(4000, (error) => {
  console.log(error);
});
Enter fullscreen mode Exit fullscreen mode

Now, we can run all this server reaching the port 4000. If you want to test, build it with tsup or any other way that you want, like ts-node.

Rendering UI

Now, what we need is a way to render our React components, right? So the idea is to use the solutions provided by react-dom/server and handle it. Let's see the code below:

// index.ts
import Koa from 'koa';
import Router from '@koa/router';
import http from 'http';
import { renderToPipeableStream } from 'react-dom/server';

import { App } from './App';

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

router.get('/(.*)', 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';
  }
});

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

const server = http.createServer(app.callback());

server.listen(3000, () => {
  console.log('Server listening on port 3000');
});
Enter fullscreen mode Exit fullscreen mode

What we're doing here is: using the onShellReady function, we're inserting the chunk into the writable stream and finishing the response. It'll resolve the Suspense component and display it for the final user.

We need to wrap this flow into a Promise to ensure that Koa will wait until the resolution of the promise, which implies the resolution of the writable stream, returning the right HTML code.

The App component is your root project, in the case of the example, I just write a Hello, world! as you can see here. But you can insert anything you want.

You can see here the code.

In Conclusion

With this simple setup, you can have a powerful tool in your hand that provides our final users a better experience giving them a better SEO, a fast loading time, and other useful things that SSR brings to us for some specific cases.

But a valid question is: Why not use a template like Handlebars? For the use case inside Woovi, templates won't help us because we would need two core points: reuse our design system and ensure the usage of GraphQL in our payment link.

Inside Woovi, our entire codebase is managed by GraphQL using the Relay client framework. To ensure the best UX possible for our final user, we give some useful features in our payment link, like the real-time update after paying a charge. It's all handled by our GraphQL, which won't be solvable by templates in our use case.

This is an overview about how we handle internally our SSR stuffs. To be a production-ready code has some incrementally stuffs that needs to be done.

About us

Woovi is a Startup that enables shoppers to pay as they like. To make this possible, Woovi provides instant payment solutions for merchants to accept orders.

If you want to have good challenges like creating a powerful solution using Server-side rendering + GraphQL from scratch, join us!

We are hiring!

💖 💪 🙅 🚩
noghartt
Guilherme Ananias

Posted on August 30, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related