├── .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 | --------------------------------------------------------------------------------