Ionic SSR with React: An adventure
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. React is an official flavor of Ionic.
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:
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.
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
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 world 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.
In the end, keep this in mind. Complexity must be justified!