How to use ESLint to improve your workflow
ESLint ecosystem can be super handy for JS and TS codebases, but are you using it right? Keep reading to learn about useful ESLint rules and a get a little bit more out it.
Summary
Five weeks ago, our friend Vincent Composieux wrote an awesome article about migrating a React client-side application to server-side with Next.JS. But, sometimes you want to do it the plain vanilla way in order to take control over your workflow. Therefore, this article's purpose is to climb up the steps of developing an SSR React app from scratch.
Let me state the obvious, SSR provides a better SEO as compared to a traditional client-side rendered SPA (single page application). Considering the fact that most SPAs are asynchronous, which means that they need time to load their data, they cannot be fully indexed by search engine crawlers, because these crawlers won’t wait for them to render. This fact can be taken as a given which must be unraveled. SSR also provides a faster access to your content, thanks to server rendered markup which doesn’t depend on your assets to be loaded.
There are, however, some trade-offs that you have to think about. High availability is gonna cost you, big time. As a matter of fact, you are rendering your large app in your server. This needs serious resources in terms of CPU because you're way beyond serving only static assets. Interoperability is another headache to consider. You won't be able to provide a decent experience unless you overcome browser-specific code.
I’m gonna spare you the theatrics and get straight to the point. We're going to build a React app using SSR.
Here are the prerequisites that you need:
I’m not gonna go through all the dependencies, but you can find them in the package.json
.
First things first, let's take a look at package.json
scripts section:
... "scripts": { "start": "concurrently -k -p \"[{name} - {time}]\" -c \"cyan,magenta\" -n \"client,server\" \"yarn start-client\" \"yarn start-server\"", "build": "yarn run clean && webpack -p --progress", "clean": "rm -rf ./public/*", "start-client": "webpack --debug --watch", "start-server": "nodemon --exec ts-node src/server/index.ts --watch ./src -e ts,tsx" }, ...
As you can see, we are concurrently
running 2 instances of Node, one for the server and the other for webpack. We aren't using webpack-dev-server
because we don't need it. We just have to watch the files and compile them to be bundled. This will allow us to serve them statically from the public path.
As we've seen earlier, we're gonna split our code into 2 bundles, main.js
and vendor.js
. The webpack config is as follows:
const path = require('path');
const webpack = require('webpack');
const NODE_ENV = process.env.NODE_ENV ? process.env.NODE_ENV.toLowerCase() : 'development';
const mode = NODE_ENV === 'development' ? 'dev' : 'prod';
module.exports = {
devtool: 'source-map',
entry: {
main: path.join(__dirname, 'src', 'client', `index.${mode}`),
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: 'vendor.js',
minChunks(module) {
const context = module.context;
return context && context.indexOf('node_modules') >= 0;
},
}),
],
output: {
path: path.join(__dirname, 'public'),
filename: '[name].js',
chunkFilename: '[name].js',
publicPath: '/',
library: '[name]',
libraryTarget: 'umd',
umdNamedDefine: true,
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
module: {
rules: [
...
],
},
};
In the output we specify the path to the directory into which Webpack is gonna place the bundles path.join(__dirname, 'public')
. This is the same place from where the server will serve the files.
The kind of output you will have is similar to this:
$ yarn start yarn start v0.27.5 $ concurrently -k -p "[{name} - {time}]" -c "cyan,magenta" -n "client,server" "yarn start-client" "yarn start-server" [server - 2017-10-11 12:57:29.361] [nodemon] 1.12.1 [server - 2017-10-11 12:57:29.368] [nodemon] to restart at any time, enter `rs` [server - 2017-10-11 12:57:29.368] [nodemon] watching: /Users/kamal/code/eleven-labs/articles/ssr/the-wilson-post/src/**/* [server - 2017-10-11 12:57:29.368] [nodemon] starting `ts-node src/server/index.ts` [client - 2017-10-11 12:57:29.619] [client - 2017-10-11 12:57:29.619] Webpack is watching the files… [client - 2017-10-11 12:57:29.619] [client - 2017-10-11 12:57:30.226] ts-loader: Using typescript@2.5.3 and /Users/kamal/code/eleven-labs/articles/ssr/the-wilson-post/tsconfig.json [server - 2017-10-11 12:57:30.508] ==> 🌎 Listening on port 9001. Open up http://localhost:9001/ in your browser. [client - 2017-10-11 12:57:38.220] Hash: 86f91e22ceb773dceb73 [client - 2017-10-11 12:57:38.220] Version: webpack 3.6.0 [client - 2017-10-11 12:57:38.220] Time: 8609ms [client - 2017-10-11 12:57:38.220] Asset Size Chunks Chunk Names [client - 2017-10-11 12:57:38.220] main.js 21.8 kB 0 [emitted] main [client - 2017-10-11 12:57:38.220] styles.js 447 bytes 1 [emitted] styles [client - 2017-10-11 12:57:38.220] vendor.js 2.29 MB 2 [emitted] [big] vendor [client - 2017-10-11 12:57:38.220] main.js.map 21.2 kB 0 [emitted] main [client - 2017-10-11 12:57:38.220] styles.js.map 682 bytes 1 [emitted] styles [client - 2017-10-11 12:57:38.220] vendor.js.map 2.76 MB 2 [emitted] vendor
Let's look at the client's structure:
├── src | ├── client | | ├── actions | | ├── assets | | ├── components | | ├── containers | | ├── epics | | ├── reducers | | ├── routes | | ├── services | | ├── store | | ├── constants.ts | | ├── index.dev.tsx | | └── index.prod.tsx | ├── ...
This is a typical React/Redux app that follows Dan Abramov's recommendations about presentational and container components. We're going to use redux-observable
to handle side effects, and react-router
to show how routing can be done.
We are going to build an application called "the-wilson-post". It's a kind of article scheduler. It will help our communications manager with handling the massive amount of article requests from Eleven Labs engineers.
Let's look at the routes:
// routes/index.ts import App from '../containers/App'; import Home from '../containers/Home'; export default [ { component: App, routes: [ { path: '/', exact: true, component: Home, }, ], }, ];
This module exports a simple array of routes structured as a tree. This array will be fed to the renderRoutes
function in the react-router-config
module. renderRoutes
aims to mount
the route that matches the requested page URL. Below you can see how it's done in the entry file of the application:
// index.prod.jsx
import * as React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { renderRoutes } from 'react-router-config';
import { BrowserRouter } from 'react-router-dom';
import { INITIAL_STATE } from './constants';
import routes from './routes';
import createStore from './store/prod';
const store = createStore(INITIAL_STATE);
render(
<Provider store={store}>
<BrowserRouter>
{renderRoutes(routes)}
</BrowserRouter>
</Provider>,
document.getElementById('root'),
);
Notice:
BrowserRouter
uses the HTML5 history API, which makes it easier for engineers to handle UI state by using the native functions it provides like:pushState
andreplaceState
.
In React Router, routes are simple components that render their children. These children might be simple components and/or routes. That is, everything in React Router is a component. In this article, we are using react-router v4
which introduced the Dynamic Routing concept that's based on the idea of routing while rendering.
renderRoutes
is a function that takes as an argument a list of route objects. Each one of these objects contains a path
and a component
property among other properties. renderRoutes
goes through the list and matches the current requested URL to mount the appropriate component. Which makes it eligible to guarantee the dynamic aspect of routing.
Our route objects also contains a children
property, which is composed of child routes. This allows us to build components that can mount these child routes, as it can be seen in the following example where we are creating the main page component:
// client/containers/App.tsx
import * as React from 'react';
import { renderRoutes, RouteConfig } from 'react-router-config';
interface Props {
route?: RouteConfig;
}
export default class App extends React.Component<Props, object> {
render() {
const { route } = this.props;
return (
<div className="wilson-post-app">
<nav>
<h1>The Wilson Post</h1>
<div>
<Link to="/schedule-post">Schedule a post</Link>
</div>
</nav>
{route && renderRoutes(route.routes)}
</div>
);
}
}
Notice: You can do the same thing by using
<Switch />
and<Route />
components. It is recommended to see how it's done in React Router's docs.
Here is the generated tree:
<Provider> <BrowserRouter> <Router> <Switch> <Route> <App> ... <Switch> <Route> <Home> ... </Home> </Route> </Switch> </App> </Route> </Switch> </Router> </BrowserRouter> </Provider>
In the previous example we've seen the app component that dynamically includes child routes. In case we reached the /
in the URL, the inclusion triggers the mounting of the Home
component, which will display a list of articles. There will be another page that will allow us to schedule a post. Here are the final routes that we will have:
// client/routes/index.ts import App from '../containers/App'; import Home from '../containers/Home'; import Scheduler from '../containers/Scheduler'; import NotFound from './NotFound'; export default [ { component: App, routes: [ { path: '/', exact: true, component: Home, }, { path: '/schedule-post', exact: true, component: Scheduler, }, { path: '*', component: NotFound, }, ], }, ];
You will find these pages in the source code. We will talk about the NotFound
route a little bit later.
We're gonna use JSON server for the sake of simplicity and fast scaffolding. We will mount it as an express middleware into the server:
// server/api/index.ts import { create, router } from 'json-server'; import * as path from 'path'; const server = create(); const apiEndpoints = router(path.join(__dirname, 'posts.json')); server.use(apiEndpoints); export default server;
For more info, see my article on json-server.
The server is a classic Express app that mounts the API middleware, along with the routes middleware, the responsibility of which is to transform React components into a string, in order to serve it to the client.
Let's look at the structure:
├── src | ├── client | | ├── ... | ├── server | | ├── api | | | ├── index.ts | | | └── posts.json | | ├── middlewares | | | ├── 404.ts | | | └── 500.ts | | ├── routes | | | └── index.tsx | | ├── templates | | | └── index.ejs | | └── index.ts
I think it's useless to show the entire file here, so the following is the most important part:
// server/index.ts ... app.use('/static', serveStatic(publicPath)); app.use('/api', api); app.use('/', routes); // error handlers app.use(handler500(app.get('env'))); app.use(handler404); ...
We are serving the static assets —namely the webpack bundles: vendor.js
, main.js
and styles.js
— along with the API and the routes. We are also handling errors by using 404 and 500 error middlewares.
The holy grail here is the renderToString
function from react-dom/server
. It takes a react component and generates its HTML string:
// server/routes/index.tsx import * as Express from 'express'; import * as React from 'react'; import { renderToString } from 'react-dom/server'; import { Provider } from 'react-redux'; import { renderRoutes } from 'react-router-config'; import { StaticRouter } from 'react-router-dom'; import routes from '../../client/routes'; import { getPosts } from '../../client/services/posts'; import createStore from '../../client/store/dev'; export default async (req: Express.Request, res: Express.Response) => { const posts = await getPosts(); const store = createStore({ posts }); const content = renderToString( <Provider store={store}> <StaticRouter location={req.url}> {renderRoutes(routes)} </StaticRouter> </Provider>, ); res.render('index', { content, data: store.getState() }); };
We are using the StaticRouter
instead of BrowserRouter
, because the location never changes in the server. And, by creating the store, we provide the app with initial data from the API by calling the getPosts
service from the client. These data are finally passed to the template index.ejs
in order to make it available to the client's store:
<!-- server/templates/index.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>The Wilson Post</title>
<!-- HTML5 shim, for IE6-8 support of HTML elements -->
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
<div id="root"><%- content %></div>
<script>
window.__INITIAL_STATE__ = <%- JSON.stringify(data) %>;
</script>
<script src="/static/vendor.js"></script>
<script src="/static/styles.js"></script>
<script src="/static/main.js"></script>
</body>
</html>
Handling 404
is a little bit tricky, it actually works with our current code. When you visit an URL that the router can't match, it triggers the asterisk *
route and mounts its component, matching the NotFound
route:
// client/routes/index.ts ... { path: '*', component: NotFound, }, ...
But, there is a problem here; unlike the client, the server won't be aware whether it was a 404
, and will serve the page with a 200
response status. To fix it, we need to ask the router to tell the server what really happened during route matching, so that the server can update the response status based on the router's result. We can do this by using the StaticRouter
's context property. The idea consists of passing a context object to the StaticRouter
so that it can be updated with the real status in the NotFound
component:
Here is the updated version that uses the context:
// server/routes/index.tsx
...
interface StaticRouterContext {
status?: number;
}
export default async (req: Express.Request, res: Express.Response) => {
const posts = await getPosts();
const store = createStore({ posts });
const context: StaticRouterContext = {};
const content = renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={context}>
{renderRoutes(routes)}
</StaticRouter>
</Provider>,
);
if (context.status === 404) {
res.status(404);
}
res.render('index', { content, data: store.getState() });
};
But, how are we gonna update the context? Here is the NotFound
component that we need to display:
// client/routes/NotFound.tsx import * as React from 'react'; export default (): JSX.Element => ( <div className="not-found"> Not found </div> );
This component needs to change the context passed from the server. To do it, we will use the Route
component to have access to the context property, so that we can change it:
We're gonna write a generic Status component that will do the work:
// client/routes/Status.tsx
import * as React from 'react';
import { Route, RouteComponentProps } from 'react-router-dom';
interface Props {
status?: number;
children?: React.ReactNode;
}
export default ({ status, children }: Props): JSX.Element => (
<Route render={({ staticContext }: RouteComponentProps<any>) => {
if (staticContext) {
staticContext.status = status;
}
return children;
}} />
);
Then we use it in our NotFound
component, and pass the right status to it:
// client/routes/NotFound.tsx import * as React from 'react'; import Status from './Status'; export default (): JSX.Element => ( <Status status={404}> <div>Not found</div> </Status> );
This way, when the server is done matching the routes, it will end up with the context having the right status, so that it can set it to the response:
// client/routes/index.tsx ... if (context.status === 404) { res.status(404); } ...
Now, when you go somewhere you're not supposed to, the server knows what status to set in the response, and will still serve you the application in order for the client to display the NotFound
component.
Redux is agnostic to the environment. This makes it pretty easy to handle, as it's used exactly the same way as in the client. We create a store and pass it to the Provider component. Then, we get its state in order to give it to the client's store. This is when it gets a little bit awkward to me. We wind up doing some messy code trying to pass the data to the client. We do this by giving the data to the index.ejs
template in order to set it as a global variable:
... <script> window.__INITIAL_STATE__ = <%- JSON.stringify(data) %>; </script> ...
Notice: I don't see how this can be done otherwise. But if someone has any idea about a better and cleaner way to do this, please let me know :-)
Here is an update that makes it possible for the client's store to take advantage of __INITIAL_STATE__
data:
// client/index.dev.tsx
...
const win: ExtendedWindow = window as ExtendedWindow;
const state = win && win.__INITIAL_STATE__ ? win.__INITIAL_STATE__ : INITIAL_STATE;
const store = createStore(state);
render(
<Provider store={store}>
<BrowserRouter>
{renderRoutes(routes)}
</BrowserRouter>
</Provider>,
document.getElementById('root'),
);
This way we are sure we have the same display in the server and the client.
SSR can be a piece of cake, at least before approaching the intricacies of the routing. You can find the entire code in Github. And, please! Feel free to give your feedback :-)
Thanks for reading!
Author(s)
Kamal Farsaoui
Web developer / Previously Founder & CEO at CSI, Co-Founder & CTO at Neiio / Coffee snob
You wanna know more about something in particular?
Let's plan a meeting!
Our experts answer all your questions.
Contact usDiscover other content about the same topic
ESLint ecosystem can be super handy for JS and TS codebases, but are you using it right? Keep reading to learn about useful ESLint rules and a get a little bit more out it.
Tutorial on the basics of NextJS for building a website.
You may not be using the React states optimally and I'll explain why