Server-side React
Server-side rendering a React application can have a lot of moving pieces. We’ll look at the basics of server rendering those applications and the needs our applications would have.
But what is server-side rendering and why should I change my application to adopt this?
Server-side rendering your React application means leveraging a server to send an initial render to your client. I like to think of this as one hand washing another. Typical client-side applications send your user an empty <div>
, a single bundle and a 🙏. This leads to your user having to wait for that bundle to load and parse before we get to an initial paint. Studies have shown, this leads to mistrust of your brand and confusion of your UI on your user’s part. Because we go to the URL, see an empty screen, then a loading state, then our application.
Consider the application below, it’s a simple list view that show’s the latest
Rationale
Single page apps, have historically use the browser as the runtime, often sending an almost empty <body>
tag and bundled JavaScript file to execute and essentially boot the application. In re-reading Isomorphic JavaScript: the future of Web Apps from 2013, a lot of the problems Spike Brehm outlined for single page applications are still relevant.
- Single page apps have a single point of failure.
- Single page apps lack effective SEO.
- Single page apps suffer performance concerns.
In 7 Principles of Rich Web Applications from Guillermo Rauch, published in November 2014, he outlines the fundamental ideals that should govern building web applications that confront indirectly the concerns from Spike Brehm. His first remedy is “Server rendered pages are not optional”.
In using a pattern for server-side rendering your application, you typically can have one hand washing the other. We get an initial render from the server, which includes our previous script tag and React “hydrates” our client and takes over from that initial static render.
This serves the purpose of being able to serve open graph tags in the <head>
, content to scrape, which beyond SEO makes sharing URLs easier, and more importantly, it gives the client some real context to your application and immediate feedback.
One Hand Washing the Other
In a basic setup, instead of calling returning some JSON data, we’re going to call renderToString
from "react-dom/server"
// server.js
import express from "express";
import React from "react";
import { renderToString } from "react-dom/server";
import App from "./components/App";
const app = express();
app.get("/*", (request, response) => {
response.send(template());
});
app.listen(3000);
function template(props) {
const app = renderToString(<App {...props} />);
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>React SSR</title>
</head>
<body>
<div id="root">${app}</div>
<script src="./client.js"></script>
</body>
</html>
`;
}
The content-type
in Express.js will infer that you’re sending pre-formed HTML not just a string of HTML and we need to start this Express server.
node server.js
Now we need to make an adjustment to the client application:
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
ReactDOM.hydrate(<App />, document.getElementById("root"));
Instead of calling ReactDOM.render
we call ReactDOM.hydrate
. This does the same thing as a render, expect better, it gives cues to the renderer as to what do with that initial render from the server.
So far that’s all the implementation we need to get started. But there are
Design Considerations
- Components only has a truncated lifecycle:
constructor()
,UNSAFE_componentWillMount()
,render()
😳. For the initial server render we don’t have the full lifecycle that we’re used to on the client. This makes a lot of dynamic work like fetching data, subscribing to a store, adding a listener of any kind, go right out the window; (well lack of awindow
really). - Essentially: we don’t have
setState()
🚫. We can initialize state in the constructor, call the initial props inside that state, but we can’t call setState inrender()
orconstructor()
. matchMedia
,localStorage
,fetch
are all gone, kinda ☠️, since we’re rendering on the server none of the things on thewindow
object. We can mock out globals likefetch
easily enough with packages like isomorphic-fetch. If we need some data from the client, safest bet is use a query parameter in the URL or in a cookie.
Fine Print
JSX isn’t valid JavaScript. So when you call your component inside of renderToString()
, you’re going to need to transpire that code first. So you will need a server bundle and client bundle.
Routing
We’re using React Router, I’m willing to bet your application uses that too. React Router uses the History API and window.location
to determine what components to render on the client. But you can do same thing on the server, so that your server render, renders the view being requested by the user.
To get started, hoist wherever you’re calling <BrowserRouter />
to the very top level where you call hydrate()
. Then, call <StaticRouter />
on the server.
ReactDOM.hydrate(
<BrowserRouter>
<App />
</BrowserRouter>
);
renderToString(
<StaticRouter>
<App />
</StaticRouter>
);
Now we can pass the URL being pinged and render just that page for the client. The request header from Express has the URL.
Side note: We have to give StaticRouter
a context object to give the client from the server. It can be an empty object.
function template({ location, ...props }) {
const context = {};
const app = renderToString(
<StaticRouter location={location} context={context}>
<App {...props} />
</StaticRouter>
);
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>SSR</title>
</head>
<body>
<div id="root">${app}</div>
<script src="./client.js"></script>
</body>
</html>
`;
}
app.get("/*", (request, response) => {
const markup = template({ location: requet.url });
response.status(200).send(markup);
});
Data-fetching & Authentication
In our example from earlier, our initial client render showed a loading spinner, when the component mounted we made a fetch
request and when data comes back from the API we render the data. But we can hoist this function that calls the API, call it on the server and avoid the loading spinner all together.
We start here by just isolating the API request.
// api.js
const URL = "https://github-trending-api.now.sh/repositories";
export const getRepos = async (...args) =>
await fetch(URL).then((res) => res.json());
// component.js
class Container extends React.Component {
state = {
repos: [],
};
async componentDidMount() {
const repos = await API.getRepos();
this.setState({ repos });
}
render() {
const { repos } = this.state;
return repos.length === 0 ? <Spinner /> : <RepoList repos={repos} />;
}
}
Then on the server:
async (request, response) => {
const { TOKEN } = req.cookies;
const data = await API.getRepos();
response.send(template({ repos: data }));
};
Now we have that data making our initial render full of content that’s valuable to the user instead of the loading spinner.
The only hole left in this is duplicate fetches. We need to check if that data exists first. The best way to do that is this detailed technique from Tyler McGinnis in “Server Rendering with React and React Router”. Essentially:
- Serialize the data in your template literal and assign it to something like
window.__INITIAL_DATA__
- Check to see if there’s something on that key
- When it’s time to actually fetch more data on the client, remove that key off the
window
In that example, we’ve separated the data fetching from the component. Sometimes you’ll want to see maintain coupling a component’s data fetching to the implementation. It is possible but it’s a little 😖. Next.js uses this pattern with a static method called getInitialProps
which abstracts the hardship of coupling to the framework level. In order to couple, we need to be call that externally from the component anyway.
You can also make authenticated API calls if you’re using a token based system. When you store your token in your cookie, your 🍪-based authentication allows you to be able to grab your token from the server and call the same endpoint as that user.
async (request, response) => {
const { TOKEN } = req.cookies;
const data = await fetch("endpoint/", {
headers: { Authorization: TOKEN },
}).then((res) => res.json());
response.send(template({ data }));
};
Hooks and Suspense
Hooks are a new API coming to React. They make previously stateless function components, stateful without the trouble of the class API and lifecycle methods. Given our design considerations, Hooks, give their default value when rendered on the server.
const ThemeContext = React.createContext({ dark: true });
function App(props) {
const [open, setOpen] = React.useState(false);
const context = React.useContext(ThemeContext);
return (
<div
className="App"
style={{
background: context.dark ? "black" : "white",
color: context.dark ? "white" : "black",
}}
>
<h1>Hello {props.name}</h1>
{open && "I AM OPEN"}
<button onClick={() => setOpen(!open)}>Toggle</button>
</div>
);
}
In this example, theme is set to true
, the open state is set to false
.
Suspense is another API coming to React. It allows you to suspend rendering your component until a resource is available. I honestly can’t get a good pulse on the implementation of server-side rendering. My hope is that it simplifies things by just making the data fetching functions fire on the server without any configuration.
In this video, Andrew Clark details an experimental implementation of Suspense and server-side rendering. But I’m still waiting for a clearer implementation to emerge 🍿⏱🌷👏⚡️🔥.
Super Professional Expert Opinion
Don’t travel this process alone 🚀. You should be using a framework 🛠 to handle the rougher edges of things like routing, data-fetching and bundling. I recommend Next.js.
But here are a couple other frameworks that stand out:
Not surprisingly, Next.js handles a lot of the issues I’ve been describing in a very elegant way to shift your focus back to your product vs managing all these concerns. Besides this, it works well today without waiting for new APIs to land.
Slides
I was very glad to have given this as my first talk at Seattle.js. You can see the slide deck here, (I totally built this with mdx-deck).