Introduction
Not always you, as a software developer, can run away from a project's scope change. Poor requirements gathering can lead you to these situations, and here I'll show how I handled a specific case where I need to change a project created with CRA (Create React App) to support SRR (Server Side Rendering).
At first, I considered Nextjs, which is a robust solution for SSR, but the problem was: lots of rewriting would be necessary. Nextjs is a framework, and as so it has its specific way to implement things. The code impact would be big, big enough to make me search for something new and more affordable for my current situation.
So I found Razzle. As you can read in the Razzle project description, it specifically aims to feel the gap in buy you into a framework
or setting things yourself.
Solution
Similar to CRA, Razzle has its own create-razzle-app
. The first step was simple as:
npx create-razzle-app my-app-name
I created a new app and throw my app files inside it, but you can merge what was generated into your existing app (although this can be a bit more arduous).
Razzle works like a charm but, although it has a low code impact on the codebase, there is some impact already expected because SSR requires some alterations. So here is what I needed to focus on:
- Routes;
- Replace what was using js
window
object; - Styles.
First, it is necessary to know when you are on the server or on the browser. The helper below was used for this purpose.
export const isServer = !(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);
Routes
To be able to navigate back/forward previously accessed pages, history
from React Router was being used. The following alteration was necessary:
From
export const history = createBrowserHistory();
To
export const history = isServer
? createMemoryHistory({
initialEntries: ['/'],
})
: createBrowserHistory();
Using the createBrowserHistory
function in the server throws you the error Invariant failed: Browser history needs a DOM
. Obviously, no DOM is available there, so we used the createMemoryHistory
function that doesn't require a DOM.
Replacing the window
object functions
The window
object was being used in some parts of the code where the localStorage
was being called. The localStorage
was being used to store login sessions and a shopping cart id, so the first step was to find a replacement for it: cookies.
Cookies can be accessed by the server, and although I didn't need to do so, it wouldn't break the app (what otherwise would happen using the window
object). React Cookies filled my needs, and I encapsulated all my cookies interaction in a class I called CookieUtility
.
Replacing localStorage
with my CookieUtility
solved the question here, and I wanna show the only one that was tricky at first: the PrivateRoute
component. So the alteration was:
From
...
const PrivateRoute = (props) => {
const token = localStorage.getItem(BrowserStorageKeyEnum.Jwt);
let isTokenExpired = false;
if (token) {
const decodedJwt = jwt.decode(token);
const currentTimeInSeconds = moment(Math.floor(Date.now() / 1000));
const expirationTimeInSeconds = decodedJwt.exp - currentTimeInSeconds;
if (expirationTimeInSeconds <= 0) isTokenExpired = true;
}
if (token && !isTokenExpired) {
return <Route {...props} />;
} else {
return (
<Redirect
to={{
pathname: RouteEnum.Login,
state: { from: props.location }
}}
/>
);
}
};
...
To
...
export default function PrivateRoute(props) {
if (isServer) return <LoadingPageIndicator isLoading={true} />;
else {
const jwt = CookieUtility.getJwt();
if (!!jwt) {
return <Route {...props} />;
} else {
return (
<Redirect
to={{
pathname: RouteEnum.Login,
state: { from: props.location },
}}
/>
);
}
}
}
Keep in mind that that the new version of the PrivateRoute
is more succinct because the code was refactored, and all the time-wise logic was put in the CookieUtility
, defining cookies expiration time.
What you should pay attention to is the first line of the new PrivateRoute
component function: if in the server, just display a loading indicator. If you are doing so for SEO (Search Engine Optimization) purposes, this would be a problem, but in my case, no private route exists whit this intention, just public ones, so this trick works just fine.
Styles
The app was being implemented using Styled Components that already comes with an integrated solution for SSR, allowing you to load all the required styles for the target page and put it at the end of your <header>
tag in the server.js
generated by Razzle.
import { ServerStyleSheet } from 'styled-components';
...
server
.disable('x-powered-by')
.use(express.static(process.env.RAZZLE_PUBLIC_DIR))
.get('/*', (req, res) => {
const sheet = new ServerStyleSheet();
const styleTags = sheet.getStyleTags();
...
res.status(200).send(
`<!doctype html>
<html lang="">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta charset="utf-8" />
<title>Welcome to Razzle</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
${assets.client.css ? `<link rel="stylesheet" href="${assets.client.css}">` : ''}
${
process.env.NODE_ENV === 'production'
? `<script src="${assets.client.js}" defer></script>`
: `<script src="${assets.client.js}" defer crossorigin></script>`
}
${styleTags}
</head>
`
...
Conclusion
This post showed how I migrated from a normal React app created with CRA to an SSR app, using Razzle to accomplish this. It was not done with the intention to work as a tutorial but to show you a path you can follow if you find yourself in the same situation as the one described in the introduction of this post, highlighting the steps that took me some time to understand how to overcome them.
It was worthed to use Razzle? I definitely would say yes. It was possible to migrate a middle-size app to work with SSR in a short time. The steps I described in the solution section were actually the only ones that forced me to change more large chunks of code, and besides that, I only needed to remove external libs that used the window
object, but that is expected if you're dealing with SSR (the migration process can be harder depending on how much you relly on those libs).
At the moment this post was written, Razzle is quite an active project, and there are many plugins being developed for it. For instance, there is a plugin you can use to easily handle PWA.
This is it! If you have any comments or suggestions, don't hold back, let me know.
Options if you like my content and would like to support me directly (never required, but much appreciated):
BTC address: bc1q5l93xue3hxrrwdjxcqyjhaxfw6vz0ycdw2sg06
Top comments (0)