I know, I know. This title sounds cocky. In fact, it makes a lot of sense if you think about it. I have been asked multiple times by my friends from our local React meetup group I am organizing or by teams I am helping to develop their applications for a starter, boilerplate or a setup. This post is a result of another such question. Most of the time it is one of the two scenarios.
In the first scenario, developers are looking for a way to start a project and this is the problem create-react-app
is trying to address. It is an official command line tool which is a recommended way to start a react project. It works well for people who are starting with React as it abstracts a lot of complexity connected to the configuration of babel, webpack etc. For more serious, production use when you require having more control over your build, create-react-app
provides also an eject
script which unpacks a lot of logic initially hidden in react-scripts
package. At that point, complexity initially hidden is often intimidating and that is only the tip of the iceberg. Now you probably would like to have library for state management, server side rendering and so on. Nevertheless, if you want to test a new library or learn about React create-react-app
is a great choice.
In the second scenario, application is already under development, but no one on the team is comfortable with it and perceives "code around webpack" as strange and extraneous. The core of the application is one of the popular boilerplates cloned at the beginning of the project. Often there was never a time to understand it from the ground up and get a holistic picture of what is going on.
This a little long introduction leads us to the merit of what I am trying to say. I believe that the best boilerplate is the one you do yourself. Initial investment in putting puzzles together pays off in a confidence, ease of solving later problems and overall maintenance time.
So, what I am going to do is take my minimal setup (webpack 2 + hot reloading) and start building upon it. At the end I will have Redux, React Router and Helmet playing nicely together on both client and server.
If you do not like it, that is fine, it was created to make my life easier and does that great. You can always use create-react-app
instead and eject
.
Entire code is available on GitHub: react-boilerplate-lite#not-so-lite.
React Router
Routing is an important part of our setup as it has to be integrated on both client and server side. React Router is de facto standard way to manage routes in react applications and it supports server side rendering.
I am going to use the most up-to-date version, React Router v4, but if you are already using the previous version follow instructions from corresponding docs or update. Migration paths are straightforward, so I would recommend updating. We want to use the same routes definition on both client and server, but we need different routers. On the client side application should be wrapped in a <BrowserRouter>
and on the server side in a <StaticRouter>
. Let's start with defining routes in the top-level component.
// Navigation.jsx
import { Link } from "react-router-dom";
const Navigation = () => (
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/posts">Posts</Link></li>
</ul>
);
// App.jsx
import { Route, Switch } from "react-router-dom";
const App = () => (
<div>
<Navigation />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/posts" component={Posts} />
</Switch>
</div>
);
Now, let's edit render function in bundle entry file and wrap routes in BrowserRouter
. BrowserRouter
is using HTML5 history API, so there is need for explicitly passing history like in previous React Router version. Implementation supporting hash portion of the URL has been moved to HashRouter
. We will get back to render function later when integrating Redux.
// index.js
import { BrowserRouter } from "react-router-dom";
function render(Root) {
ReactDOM.render(
<AppContainer>
<BrowserRouter>
<Root />
</BrowserRouter>
</AppContainer>,
document.getElementById("root"),
);
}
Server Side Rendering: Router
To render application on the server we need few changes to how project is built. We could try to use app's source files directly in Node. Major problem with this approach is that it becomes hard to supplement webpack's features like chunks and various loaders. Whether it is possible or not depends on to what extend your application is coupled with the build process.
I am using css-loader
modules, I like to require
my images and video files and then use a list of assets in my Service Workers for caching. I would say my apps are tight to webpack. The best solution in such case is to create separate server build working when run by Node. Webpack config for server is similar to the production one with different entry and output.
{% gist f9ba82a312252ca56b02686bc2972462 webpack.server.config.1.js %}
Once we have a suitable build we can create render function used on the server. I am using HTMLWebpackPlugin
for generating index.html
file and we can use it as a template on the server. We could also use templating engine, but we can get away without it. Depending on whether we serve files from memory during development or from hard drive on production we need a different way to read this file. To read template during development we use webpack-dev-middleware
. As JSX is not supported natively in Node I am using React.createElement
to wrapp App component with StaticRouter. During development we want to make sure that render is always using the newest version of the server bundle, so we delete it from require.cache
. We do not have to worry about it in production as we do one build per release.
// webpack.server.config.js
module.exports = {
// ...
entry: {
app: path.resolve("src/common/App/App"),
},
output: Object.assign({}, config.output, {
filename: "[name].server.js",
libraryTarget: "commonjs",
}),
// ...
};
// render.js
function getTemplate() {
if (process.env.NODE_ENV === "production") {
return fs.readFileSync(path.resolve("build/index.html"), "utf8");
}
return devMiddleware.fileSystem.readFileSync(path.resolve("build/index.html"), "utf8");
}
function render(req, res) {
const context = {};
const { default: App, Helmet } = require("../../build/app.server");
if (process.env.NODE_ENV !== "production") {
delete require.cache[require.resolve("../../build/app.server")];
}
const template = getTemplate();
const body = ReactDOMServer.renderToString(
React.createElement(StaticRouter, { location: req.url, context },
React.createElement(App)
)
);
const html = template
.replace("<div id=\"root\"></div>", `<div id="root">${body}</div>`);
res.send(html);
}
// server.js
// ...
const app = express();
app.get("*", render);
app.listen(process.env.PORT)
Although the page is rendered, server is not aware of 404 errors and cannot perform a redirect. When page is not found or redirect should be performed page is going to be rendered like usually with 200 state. To fix it we can use context which can be modified mutating staticContext
which is passed to each route on render. Let's start with creating a generic component which allows setting correct status code.
// Status.jsx
import { Route } from "react-router-dom";
const Status = ({ status, children }) => (
<Route
render={({ staticContext }) => {
if (staticContext) staticContext.status = status;
return children;
}}
/>
);
We can use Status generic component for Not Found page and redirects.
// NotFound.jsx
import Status from "../Status/Status";
const NotFound = () => (
<Status status={404}>
<div>
<h1>Not Found</h1>
</div>
</Status>
);
// RedirectWithStatus.jsx
import { Route, Redirect } from "react-router-dom";
const RedirectWithStatus = ({ status, from, to }) => (
<Route
render={({ staticContext }) => {
if (staticContext) staticContext.status = status;
return <Redirect from={from} to={to} />;
}}
/>
);
// App.jsx
const App = () => (
<div>
<Navigation />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/posts" component={Posts} />
<RedirectWithStatus
status={301}
from={"/home"}
to={"/"}
/>
<Route component={NotFound} />
</Switch>
</div>
);
On the server in render function we have to check whether context
has a url set. If it has that means redirect was matched by router. Then we can perform redirect with correct status. If context.url
is not set then we just render application with given status, 200 by default.
// render.js
const Helmet = require("react-helmet").default;
function render(req, res) {
// ...
if (context.url) {
res.redirect(context.status, context.url);
} else {
res.status(context.status || 200).send(html);
}
res.send(html);
}
Helmet
Helmet is a great little library providing react component for managing all changes to document head. We can use it for setting title or different meta tags.
The Open Graph protocol which Facebook's using to better understand your website won't work with Single Page Applications. Both Facebook and Twitter crawlers do not evaluate JavaScript. This is where Server Side Rendering shines.
// Home.jsx
import Helmet from "react-helmet";
const Home = () => (
<div>
<Helmet>
<title>Home</title>
</Helmet>
<h1>Home</h1>
</div>
);
// render.js
const Helmet = require("react-helmet").default;
function render(req, res) {
// ...
const helmet = Helmet.renderStatic();
const html = template
.replace("<div id=\"root\"></div>", `<div id="root">${body}</div>`)
.replace("</head>", `${helmet.title.toString()}</head>`);
// ...
}
After page is rendered to string we can obtain plain HTML strings from Helmet and put them into our template. The one caveat is that we have to use the same library instance. To do that we could use bundled version of Helmet or do something opposite, exclude react-helmet
from the bundle making it an external dependency. Without this step Helmet would just render an empty element as an application would be rendered with bundled instance and renderStatic
would be called on the instance from node_modules
.
// webpack.server.config.js
module.exports = {
// ...
externals: ["react-helmet"],
// ...
};
That is it!
Redux
For this setup I am going with Redux as it provides a solution for preloading state which makes things much simpler when server rendering. You can do the same with MobX, but due to not centralized state (multiple observables) it requires more glue code.
I am not going to use any library for effects/async actions like redux-thunk, redux-loop, redux-saga or redux-observable as this is a per-project changing dependency. As far as I am concerned it does not make much sense to include any in a boilerplate. On the other hand, if you are a true believer of any of those, go ahead! Each library has a good documentation and plenty of examples so you should not have any problems to set it up. You can always google some boilerplates which implemented one of those for the sake of reference.
// rootReducer.js
import { combineReducers } from "redux";
import posts from "../posts/postsReducer";
const rootReducer = combineReducers({ posts });
export default rootReducer;
// index.js
const store = createStore(rootReducer, window.__PRELOADED_STATE__);
function render(Root) {
ReactDOM.render(
<AppContainer>
<Provider store={store}>
<BrowserRouter>
<Root />
</BrowserRouter>
</Provider>
</AppContainer>,
document.getElementById("root"),
);
}
render(App);
if (module.hot) {
module.hot.accept("./common/App/App", () => {
render(App);
});
module.hot.accept("./common/rootReducer", () => {
store.replaceReducer(rootReducer);
});
}
Later on we make preloaded state globally available. Do not forget about replacing store's reducer once it changes.
Server Side Rendering: Redux
Server Side Rendering with Redux comes down to providing preloaded state for a given route. There are few possible approaches. Data dependency might be defined by a current Route or resolved by particular route handler on the server. I am not opinionated, which approach you choose highly depends on whether your Node application can access database. If that is not the case letting route decide has an advantage of defining required data (e.g. through GraphQL fragment) or requests (e.g. axios is universal) in one place.
// server.js
// ...
app.get("/posts", (req, res) => {
const posts = { list: loadPosts() };
render(req, res, { posts });
});
// ...
To preload state we need to create a store, to do that we need a root reducer. Let's add root reducer as a second entry point.
// webpack.server.config.js
module.exports = {
// ...
rootReducer: path.resolve("src/common/rootReducer"),
// ...
};
Store is created in render function with preloaded state applied. Root reducer also shouldn't be cached so a new version is used every time. If you use connect from react-redux you need to wrap the application in Provider and pass a store in props. Finally, we have to make state globally available, so it can be picked up by store created on the client side.
// render.js
function render(req, res, preloadedState) {
const context = {};
const { default: App } = require("../../build/app.server");
const { default: rootReducer } = require("../../build/rootReducer.server");
if (process.env.NODE_ENV !== "production") {
delete require.cache[require.resolve("../../build/app.server")];
delete require.cache[require.resolve("../../build/rootReducer.server")];
}
const store = createStore(rootReducer, preloadedState);
const template = getTemplate();
const body = ReactDOMServer.renderToString(
React.createElement(Provider, { store },
React.createElement(StaticRouter, { location: req.url, context },
React.createElement(App)
)
)
);
const helmet = Helmet.renderStatic();
const html = template
.replace("<div id=\"root\"></div>", `<div id="root">${body}</div>`)
.replace("</head>", `${helmet.title.toString()}</head>`)
.replace("</head>", `<script>window.__PRELOADED_STATE__=${JSON.stringify(preloadedState)};</script></head>`);
// ...
}
Summary
It is a lot of steps and it is not trivial to put it together! That said, it pays off, with interest. Entire code is available on GitHub: react-boilerplate-lite#not-so-lite.
If you do not mind giving away the fun of setting it up and/or you are intimidated by complexity you can consider using next.js. You will the pay price of being coupled with the framework, but credit should be given where it is due. Guys from ZEIT are doing great work in Open Source!
Photo by rawpixel on Unsplash.