DEV Community

loading...
Cover image for Migrating from Create React App to SSR with Razzle

Migrating from Create React App to SSR with Razzle

rwehresmann profile image Rodrigo Walter Ehresmann Updated on ・4 min read

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
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

To

export const history = isServer
  ? createMemoryHistory({
      initialEntries: ['/'],
    })
  : createBrowserHistory();
Enter fullscreen mode Exit fullscreen mode

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 }
        }}
      />
    );
  }
};

...
Enter fullscreen mode Exit fullscreen mode

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 },
          }}
        />
      );
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
`
...
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)

pic
Editor guide