Isomorphic Pokemon (Code Splitting, Lazy Loading, React Router 4, SPA, SSR) (Part 2)

Yahya Sahaja
5 min readMar 17, 2020

--

Isomorphic Example

Hi everyone!

Today I’m gonna continue the last post about enhancing the Pokemon App. The last post is about creating a Pokemon app using React Context API + Typescript + PWA. Now continue the last discussion, making this app could be rendered on the server, but keep it Single Page App.

So, what is Isomorphic app? It’s a server-rendered web app with a single page application, you could read good explanations about it here. So, the most important thing to turn our Pokemon App to be Isomorphic app is Tweaking. As we have implemented create-react-app, then we could just be serving the rendering on the server based on the provided the SPA app, and implementing all the requirements fitting on isomorphic such as routing, pre-data-fetching, and styling. The objective is we want to render everything on the server just like the client did before, then send the result to the client, then everything handled on the SPA for user experience.

Okay, just go ahead, the first step, I’m thinking about creating the configuration for the server react app rendering using webpack. It’s used to build the application so node js can render our SPA to be sent to the client.

Adding Server

We’re gonna use node js to serve the server. So firstly install using this command

yarn add node nodemon express

Prepare SPA for SSR

One thing we could concern at the first step, we need to prepare our Singe Page Application to be ready for Server Side Rendering. So, the first problem we are facing is about Routing. The SPA code before is using BrowserRouter from react-router-dom to manage routing in our app. Then, to make the SPA could rendered on the server, we have to make it as StaticRouter, but we won’t leave the benefits from BrowserRouter (to make our app still be able to be Client-Side Rendering after initial page load). Inspired by this article, I’ve modified our SPA.

First, change our AsyncComponent to be able to load the chunk, it’s using the closure approach to wrap the Component (that will be loaded) to be rendered once it’s ready. This AsyncComponent is used to create our custom page lazy-loading and handle the page loading itself.

Then create a function to ensure all routes match the current URL (both on server matching and client matching). So that the server could load it and render it as a string (we’ll talk about this later).

Next, we’re gonna create routes.tsx file containing all routing configuration.

You could notice that we are using import(), it returns Promise to load the desired component, this is how code-splitting implemented. We could also pass the Redirect component to tricky redirect our static route.

Next step, tweak our index.tsx (the top-level component) to can be accessed from the server, and pass the props given.

There’s renderOnServer function containing location and serverProps arguments that calling ensureReady passing the routes config and location parameters, then render the StaticRouter. If you noticed, there’s (typeof window !== ‘undefined’) conditional, it tells that if it’s rendered on the server, just ignore rendering with BrowserRouter. And because node js has no reference to browser window , we need to add this conditional for anything in our code that needs the browser capabilities such as lazy load image, service worker, etc. You could also be noticed that we’re using React.hydrate() instead of React.render(). It’s because we want to re-hydrate our react app to be matched from the server-rendered.

Then, modify our view-model (the context) to calling reusable repository (data fetching). So the server could call the data too, this is optional, if we assume that the server reading the SQL, we don’t have to do this. I mean, just create a function on the server to read the SQL as the data to be served on pre-rendered. So, add the repository file.

Well Done

Our SPA now ready to be used on the server! Then put everything on server.ts (place it on the root)

Here, we are creating middleware for every request needed to be pre-rendered on the server corresponding the route that might fetch that data: pokemon list at the first page, and pokemon details.

Basically, to render our SPA code on the server, we actually can just use ReactDOMServer.renderToString() from react-dom/server . But we need to provide the data and fetching the styles to be rendered. So we could see app.get(‘*’, () => {}). It’s a route for rendering our react SPA, containing the rendering code

const ReactComponent = await renderOnServer(req.url, props);

And also notice this

const sheets = new ServerStyleSheets();const styledSheets = new ServerStyleSheet();const result = ReactDOMServer.renderToString(  styledSheets.collectStyles(sheets.collect(ReactComponent)));
const cssStrig = sheets.toString();
const html = template .toString() .replace('{{content}}', result) .replace('.material-ui{margin:0}', cssStrig) .replace('</head>', `${styledSheets.getStyleTags()}</head>`);res.send(html);res.end();

Sheets object will fetch all the styles generated from material UI, and styledSheets the styled generated from a styled component. Why? Because material-ui creates the style tag on rendering and also styled component did. For styling, you could notice how the framework treats it as server-rendered on their documentation. And after everything is ready, replace the HTML template to put our content and styles.

If you noticed, the HTML template and static route is got from here

app.use(express.static(path.resolve('./build')));
const template = fs.readFileSync(path.resolve('./build/index.html'));

So, we need to build the app, because we’re just aiming to utilize our bundled SPA to be rendered on server. So, if we want to develop or build the server, we still need to build the SPA first.

Webpack Configuration

This is the configuration file we could use

We’re using awesome-typescript-loader, node-style-loader, css-loader, and html-webpack-plugin. I’m not gonna talk about this too much, the core of the config is entry, resolve, output, module, and plugins. You could read all the configs from Webpack documentation. But we could notice externals, target, and watch config. The externals config is containing nodeExternals(), it ensures that we’re not gonna compile everything on node_modules, so the compile time will be more efficient because we’ve put that node_modules in our app. The target is node, because we are compile this as a node targetted. Then the watch config involves the mode given by argv to ensure that the build is for production or development.

Last, set up our npm script

Here, we are ready to develop or build the server. To run the development mode, we can just run yarn start:server:dev and to run the production (using pm2) just run yarn start:server

This is the repository: https://github.com/yahyasahaja/pokemon and this is the demo: https://pokemon.ngopi.men.

Once more, it’s optional whether you want to use service worker depending on the requirements. There’s also another configuration for managing proxy and cache itself, I’ll talk about this on the other day.

At last but definitely not least, I recommend to config our app manually, not using create-react-app, so we have more flexibility based on the requirements (and the CRA itself recommends not to use it for server-side). And also, you could consider using a framework Next JS to develop Isomorphic app.

That’s all for this article, thank you for reading!

--

--