├── .babelrc
├── .gitignore
├── AsyncBundles.js
├── Bundles.js
├── README.md
├── client.js
├── index.js
├── lib
├── asyncComponent.js
└── syncComponent.js
├── package.json
├── routes.js
├── routes
├── Dashboard.js
├── Landing.js
└── NestedRoute.js
├── server.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "modules": false
5 | }],
6 | "stage-2",
7 | "react"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /node_modules
3 |
--------------------------------------------------------------------------------
/AsyncBundles.js:
--------------------------------------------------------------------------------
1 | // AsyncBundles.js
2 |
3 | import asyncComponent from './lib/asyncComponent';
4 |
5 | export const Landing = asyncComponent('Landing', () => import('./routes/Landing'));
6 | export const Dashboard = asyncComponent('Landing', () => import('./routes/Dashboard'));
7 | export const NestedRoute = asyncComponent('NestedRoute', () => import('./routes/NestedRoute'));
8 | /* ... other route components */
9 |
--------------------------------------------------------------------------------
/Bundles.js:
--------------------------------------------------------------------------------
1 | // Bundles.js
2 |
3 | import syncComponent from './lib/syncComponent';
4 |
5 | export const Landing = syncComponent('Landing', require('./routes/Landing'));
6 | export const Dashboard = syncComponent('Dashboard', require('./routes/Dashboard'));
7 | export const NestedRoute = syncComponent('NestedRoute', require('./routes/NestedRoute'));
8 | /* ... other route components */
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sample application: Server-Side Rendering and Code Splitting with React-Router 4
2 |
3 | The purpose of this repository is to showcase a working solution for a server-rendered, code-split app with React-Router 4. This solution is described in detail on my personal blog [here](https://blog.emilecantin.com/web/react/javascript/2017/05/16/ssr-react-router-4-webpack-code-split.html).
4 |
5 | Highlights are:
6 |
7 | - We use a simple HOC on the server, which allows us to know which chunks were involved in the render
8 | - We use a different HOC on the client which renders asynchronously
9 | - We switch between the two implementations using Webpack's `NormalModuleReplacementPlugin`
10 | - When rendering on the server, we send the list of necessary chunks down to the client
11 | - We load these chunks on the client before doing the initial render
12 |
13 | ## Running the code
14 |
15 | Should be as simple as cloning the repo and then running `npm install && npm start`.
16 |
17 | ## License
18 |
19 | MIT
20 |
21 |
--------------------------------------------------------------------------------
/client.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import {BrowserRouter} from 'react-router-dom';
4 | import {renderRoutes} from 'react-router-config';
5 |
6 | import * as Bundles from './Bundles';
7 | import {routes} from './routes.js';
8 |
9 | const splitPoints = window.splitPoints || [];
10 | Promise.all(splitPoints.map(chunk => Bundles[chunk].loadComponent()))
11 | .then(() => {
12 | const mountNode = document.getElementById('app');
13 | ReactDOM.render(
14 |
15 | {renderRoutes(routes)}
16 | ,
17 | mountNode
18 | );
19 | });
20 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | // server.js
2 | const express = require('express');
3 | const app = express();
4 | app.use(express.static('build'));
5 |
6 | const {render} = require('./build/server.js');
7 |
8 | app.use(render);
9 |
10 |
11 | app.listen(5000);
12 |
--------------------------------------------------------------------------------
/lib/asyncComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function asyncComponent(chunkName, getComponent) {
4 | return class AsyncComponent extends React.Component {
5 | static Component = null;
6 |
7 | static loadComponent() {
8 | return getComponent().then(m => m.default).then(Component => {
9 | AsyncComponent.Component = Component;
10 | return Component;
11 | });
12 | };
13 |
14 | mounted = false;
15 |
16 | state = {
17 | Component: AsyncComponent.Component
18 | };
19 |
20 | componentWillMount() {
21 | if(this.state.Component === null) {
22 | AsyncComponent.loadComponent()
23 | .then(Component => {
24 | if(this.mounted) {
25 | this.setState({Component});
26 | }
27 | });
28 | }
29 | }
30 |
31 | componentDidMount() {
32 | this.mounted = true;
33 | }
34 |
35 | componentWillUnmount() {
36 | this.mounted = false;
37 | }
38 |
39 | render() {
40 | const {Component} = this.state;
41 |
42 | if(Component !== null) {
43 | return ();
44 | }
45 | return null; // or
with a loading spinner, etc..
46 | }
47 | };
48 | }
49 |
50 | export default asyncComponent;
51 |
52 |
--------------------------------------------------------------------------------
/lib/syncComponent.js:
--------------------------------------------------------------------------------
1 | // ./lib/syncComponent.js
2 | import React from 'react';
3 |
4 | function syncComponent(chunkName, mod) {
5 | const Component = mod.default ? mod.default : mod; // es6 module compat
6 |
7 | function SyncComponent(props) {
8 | if(props.staticContext.splitPoints) {
9 | props.staticContext.splitPoints.push(chunkName);
10 | }
11 |
12 | return ();
13 | }
14 |
15 | SyncComponent.propTypes = {
16 | staticContext: React.PropTypes.object,
17 | };
18 |
19 | return SyncComponent;
20 | }
21 |
22 | export default syncComponent;
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ssr_code-splitting_sample",
3 | "version": "1.0.0",
4 | "description": "Sample app showcasing server-side rendering and code-splitting with react-router 4",
5 | "main": "server.js",
6 | "dependencies": {
7 | "express": "^4.15.2",
8 | "react": "^15.5.4",
9 | "react-dom": "^15.5.4",
10 | "react-router": "^4.1.1",
11 | "react-router-config": "^1.0.0-beta.3",
12 | "react-router-dom": "^4.1.1"
13 | },
14 | "devDependencies": {
15 | "babel-core": "^6.24.1",
16 | "babel-loader": "^7.0.0",
17 | "babel-preset-env": "^1.4.0",
18 | "babel-preset-react": "^6.24.1",
19 | "babel-preset-stage-2": "^6.24.1",
20 | "webpack": "^2.5.1",
21 | "webpack-node-externals": "^1.6.0"
22 | },
23 | "scripts": {
24 | "test": "echo \"Error: no test specified\" && exit 1",
25 | "start": "webpack && node index.js"
26 | },
27 | "author": "Emile Cantin",
28 | "license": "MIT"
29 | }
30 |
--------------------------------------------------------------------------------
/routes.js:
--------------------------------------------------------------------------------
1 | // routes.js
2 |
3 | import {Landing, Dashboard, NestedRoute} from './Bundles';
4 |
5 | export const routes = [{
6 | component: Landing,
7 | path: '/',
8 | exact: true
9 | }, {
10 | component: Dashboard,
11 | path: '/dashboard',
12 | routes: [{
13 | component: NestedRoute,
14 | path: '/dashboard/nested'
15 | }],
16 |
17 | /* ... other routes */
18 | }]
19 |
--------------------------------------------------------------------------------
/routes/Dashboard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Link} from 'react-router-dom';
3 | import {renderRoutes} from 'react-router-config'
4 |
5 |
6 | function Dashboard(props) {
7 | const {route} = props;
8 | return (
9 |
10 |
This is the dashboard
11 | Open nested page
12 | {renderRoutes(route.routes)}
13 |
14 | );
15 | }
16 | Dashboard.propsTypes = {
17 | route: React.PropTypes.object,
18 | };
19 |
20 | export default Dashboard;
21 |
22 |
--------------------------------------------------------------------------------
/routes/Landing.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Link} from 'react-router-dom';
3 |
4 | function Landing() {
5 | return (
6 |
7 |
This is the landing page
8 | Go to dashboard
9 |
10 | );
11 | }
12 |
13 | export default Landing;
14 |
15 |
--------------------------------------------------------------------------------
/routes/NestedRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Link} from 'react-router-dom';
3 |
4 | function NestedRoute() {
5 | return (
6 |
7 |
This is a nested route
8 |
9 | );
10 | }
11 |
12 | export default NestedRoute;
13 |
14 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOMServer from 'react-dom/server';
3 | import {StaticRouter} from 'react-router';
4 | import {renderRoutes} from 'react-router-config';
5 |
6 | import {routes} from './routes.js';
7 |
8 | export function render(req, res) {
9 | const context = {
10 | splitPoints: [],
11 | };
12 | const markup = ReactDOMServer.renderToString(
13 |
17 | {renderRoutes(routes)}
18 |
19 | )
20 | // now context.splitPoints contains the names of the chunks we used during rendering
21 |
22 | const html = `
23 |
24 |
25 |
26 | My App
27 |
28 |
29 | ${markup}
30 |
33 |
34 |
35 |
36 | `;
37 | res.send(html);
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | // webpack.config.js
2 |
3 | const webpack = require('webpack');
4 | const nodeExternals = require('webpack-node-externals');
5 |
6 | const clientConfig = {
7 | entry: './client.js',
8 | output: {
9 | path: `${__dirname}/build`,
10 | filename: 'client.js',
11 | publicPath: '/',
12 | },
13 | module: {
14 | rules: [{
15 | test: /\.js$/,
16 | exclude: /node_modules/,
17 | loader: 'babel-loader'
18 | }]
19 | },
20 | plugins: [
21 | // ...
22 | new webpack.NormalModuleReplacementPlugin(
23 | /Bundles.js/,
24 | './AsyncBundles.js'
25 | ),
26 | ]
27 | };
28 |
29 | const serverConfig = {
30 | entry: './server.js',
31 | output: {
32 | path: `${__dirname}/build`,
33 | filename: 'server.js',
34 | libraryTarget: 'commonjs2',
35 | },
36 | target: 'node',
37 | module: {
38 | rules: [{
39 | test: /\.js$/,
40 | exclude: /node_modules/,
41 | loader: 'babel-loader'
42 | }]
43 | },
44 | externals: [nodeExternals()],
45 | // Server build configuration
46 | };
47 |
48 | module.exports = [
49 | clientConfig,
50 | serverConfig
51 | ];
52 |
--------------------------------------------------------------------------------