Server-side Rendering (SSR) From Scratch with React
Guilherme Ananias
Posted on August 30, 2023
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.
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;
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);
});
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');
});
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!
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
October 23, 2024