Ionic SSR with React: An adventure
Tags: ionic, ssr, puppeteer, nextjs
Published: 2022-03-28 17:19:50

---
title: Ionic SSR with React: An adventure
published: true
description: In this article I present the main three different approaches for enabling server side rendering for Ionic with React
tags: ionic, ssr, puppeteer, nextjs
//cover_image: https://direct_url_to_image.jpg
---

## Why
React is a great library for UI and makes it easier to build UIs and greatly reduces the development time. Ionic is also a great library for developing cross-platform hybrid apps. Ionic can be easily integrated with React. As a matter of fact React actually is officially supported by the Ionic team.

### It’s not all flowers and roses.
But there’s a catch; React is a JS library, where you write your UI in JS. Which means that HTML is generated on the browser. This leads to a less than ideal experience for crawlers that index your website.

## The Adventures
### Approach 1: using ReactDOMServer in a nodejs environment
A standard way to enable SSR for a React.js app is to just use `ReactDOMServer` on the server and replace the render() method with `hydrate()` in the client so that the application will not re-render (the already rendered components) when it is served to the browser.

But there’s a problem, Ionic components use stencil.js which uses web components which is not supported by `ReactDOMServer`. So we need another solution.

Example Snippet:
```js
import express from "express";
import ReactDOMServer from "react-dom/server";
import { App } from "./App";

const app = express();
app.use(express.static(STATIC_ASSETS_DIR));

app.get("*", (req, res) => {
  const appHtml = ReactDOMServer.renderToString(<App />);
  // Assume that we have an html template
  const html = htmlTemplate.replace("^_^__APP_SHOULD_BE_HERE__^_^", appHtml);
  res.send(html);
});
```
### Approach 2: using Next.js
In most cases, this approach will work. It works like this:

- Instead of using `@ionic/react`, use `@ionic/core`
- Instead of using React components (e.g. `<IonHeader />`) use web components (e.g. `<ion-header />`)
- Initialize web components by calling `defineCustomeElements`
- And that’s it (assuming you’ve configured your project to use Next.js). For a starter repo, check this repo [moonbase-vercel-ionic](https://github.com/adaept/moonbase-vercel-ionic).

However our codebase at the time had some components that are not supported in `@ionic/core`. So we couldn't use this approach.

### Approach 3: Bringing a gun to a knife fight

The last approach which will work with anything is to use a browser. A browser supports web components, is not limited to a certain library (e.g. react, angular, or vue). So, I used puppeteer which is a library that provides a Node.js API to control chromium & firefox.

So our final workflow is as follows:

- The client sends a GET request to the server
- The server checks if the needed resource is a page or just a static asset.
- If it’s a static asset serve it with express and let Nginx add cache headers along with compressing the file.
Otherwise, check if the page is already rendered and in the cache.
- If it’s in the cache serve it as a normal static asset.
- Otherwise, run Puppeteer load the page URI.
- Wait for the page to be loaded.
- Grab the page’s HTML from Puppeteer and save it in the cache.
- Send the HTML.

This can be written in Node.js as the following:

```js
const app = express();

// Static files handler
app.get("*.*", (req, res) => res.sendFile(req.url));

// Dynamic Page handler
app.get("*", async (req, res) => {
  if (cache.has(req.url)) {
    res.send(cache.get(req.url));
    return;
  }
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(`http://localhost:${CLIENT_PORT}`, { waitUntil: "networkidle0" });
  const html = await page.content();
  res.send(html);
  cache.put(req.url, html);
  await browser.close();
});

app.listen(SERVER_PORT);

// React Server

app.use(express.static(STATIC_ASSETS_DIR));

app.get("*", (_, res) => res.sendFile("index.html"));

app.listen(CLIENT_PORT);
```
Note that we’ve used two servers in this example, one where the client (i.e. the user's browser) sends requests to and handles cache as well as serving static assets while the other is for Puppeteer to connect to, to load the application.

This is not the best solution since it'll requires heavy infrastructure for high traffic websites.

> This article was originally posted on my own blog so if you find that it has been copied, don't worry I copied it from my self.