├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo.svg
└── manifest.json
├── server
├── bootstrap.js
├── controllers
│ └── index.js
├── index.js
└── middleware
│ └── renderer.js
├── src
├── App.css
├── App.js
├── App.test.js
├── PageAnother.js
├── PageDefault.js
├── SomeComponent.js
├── index.css
├── index.js
├── logo.svg
├── registerServiceWorker.js
└── store
│ ├── appReducer.js
│ └── configureStore.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## What's this?
2 |
3 | This is a sample app to demonstrate how you can achieve both **Server Side Rendering** _and_ **Code Splitting** in a create-react-app.
4 | It supports the mini-series I've wrote on _Medium_ about this topic:
5 |
6 | 1. [Upgrading a create-react-app project to a SSR + Code Splitting setup](http://medium.com/bucharestjs/upgrading-a-create-react-app-project-to-a-ssr-code-splitting-setup-9da57df2040a)
7 | 2. [Adding state management with Redux in a CRA + SSR project](https://medium.com/bucharestjs/adding-state-management-with-redux-in-a-cra-srr-project-9798d74dbb3b)
8 |
9 |
10 | ## How can I see it in action?
11 |
12 | Just install dependencies, build the app and run the express server:
13 |
14 | ```
15 | yarn install
16 | yarn run build
17 | yarn run server
18 | ```
19 |
20 | ## Can I use this as a template for a production app?
21 |
22 | **NO!** This repo exists only to demonstrate how to achieve SSR and Code Splitting at the same time.
23 |
24 | _But... Why?_ --- The server app is as slim as it can get. It lacks even the most basic security features like XSS and CSRF.
25 |
26 | **This is not a boilerplate for a production expressjs server app!!!**
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cra-ssr-code-splitting",
3 | "version": "0.1.0",
4 | "private": false,
5 | "homepage": "./",
6 | "dependencies": {
7 | "@babel/register": "^7.0.0",
8 | "babel-plugin-dynamic-import-node": "^2.0.0",
9 | "babel-plugin-syntax-dynamic-import": "^6.18.0",
10 | "express": "^4.16.2",
11 | "file-loader": "^2.0.0",
12 | "ignore-styles": "^5.0.1",
13 | "react": "^16.0.0",
14 | "react-dom": "^16.0.0",
15 | "react-helmet": "^5.2.0",
16 | "react-loadable": "^5.3.1",
17 | "react-redux": "^5.0.6",
18 | "react-router": "^4.3.1",
19 | "react-router-dom": "^4.3.1",
20 | "react-scripts": "^2.1.1",
21 | "redux": "^4.0.0",
22 | "redux-thunk": "^2.3.0",
23 | "url-loader": "^1.0.1"
24 | },
25 | "resolutions": {
26 | "babel-core": "7.0.0-bridge.0"
27 | },
28 | "scripts": {
29 | "start": "react-scripts start",
30 | "build": "react-scripts build",
31 | "test": "react-scripts test --env=jsdom",
32 | "eject": "react-scripts eject",
33 | "server": "NODE_ENV=production node server/bootstrap.js"
34 | },
35 | "browserslist": [
36 | ">0.2%",
37 | "not dead",
38 | "not ie <= 11",
39 | "not op_mini all"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andreiduca/cra-ssr-code-splitting/909ae540f2f10604df507a5f5d64f65a31768e8b/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
14 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
34 |
35 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/server/bootstrap.js:
--------------------------------------------------------------------------------
1 | require('ignore-styles');
2 | require('url-loader');
3 | require('file-loader');
4 | require('@babel/register')({
5 | ignore: [ /(node_modules)/ ],
6 | presets: ['@babel/preset-env', '@babel/preset-react'],
7 | plugins: [
8 | 'syntax-dynamic-import',
9 | 'dynamic-import-node',
10 | 'react-loadable/babel'
11 | ]
12 | });
13 | require('./index');
14 |
--------------------------------------------------------------------------------
/server/controllers/index.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 |
3 | import serverRenderer from '../middleware/renderer';
4 | import configureStore from '../../src/store/configureStore';
5 | import { setAsyncMessage } from '../../src/store/appReducer';
6 |
7 | const router = express.Router();
8 | const path = require("path");
9 |
10 |
11 | const actionIndex = (req, res, next) => {
12 | const store = configureStore();
13 |
14 | store.dispatch(setAsyncMessage("Hi, I'm from server!"))
15 | .then(() => {
16 | serverRenderer(store)(req, res, next);
17 | });
18 | };
19 |
20 |
21 | // root (/) should always serve our server rendered page
22 | router.use('^/$', actionIndex);
23 |
24 | // other static resources should just be served as they are
25 | router.use(express.static(
26 | path.resolve(__dirname, '..', '..', 'build'),
27 | { maxAge: '30d' },
28 | ));
29 |
30 | // any other route should be handled by react-router, so serve the index page
31 | router.use('*', actionIndex);
32 |
33 |
34 | export default router;
35 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import Loadable from 'react-loadable';
3 |
4 | import indexController from './controllers/index';
5 |
6 | const PORT = 3000;
7 |
8 | // initialize the application and create the routes
9 | const app = express();
10 |
11 | app.use(indexController);
12 |
13 | // start the app
14 | Loadable.preloadAll().then(() => {
15 | app.listen(PORT, (error) => {
16 | if (error) {
17 | return console.log('something bad happened', error);
18 | }
19 |
20 | console.log("listening on " + PORT + "...");
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/server/middleware/renderer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOMServer from 'react-dom/server'
3 | import Loadable from 'react-loadable';
4 | import { Provider as ReduxProvider } from 'react-redux'
5 | import { StaticRouter } from 'react-router';
6 | import { Helmet } from 'react-helmet';
7 |
8 | // import our main App component
9 | import App from '../../src/App';
10 |
11 | // import the manifest generated with the create-react-app build
12 | import manifest from '../../build/asset-manifest.json';
13 | // function to extract js assets from the manifest
14 | const extractAssets = (assets, chunks) => Object.keys(assets)
15 | .filter(asset => chunks.indexOf(asset.replace('.js', '')) > -1)
16 | .map(k => assets[k]);
17 |
18 |
19 | const path = require("path");
20 | const fs = require("fs");
21 |
22 |
23 | export default (store) => (req, res, next) => {
24 | // get the html file created with the create-react-app build
25 | const filePath = path.resolve(__dirname, '..', '..', 'build', 'index.html');
26 |
27 | fs.readFile(filePath, 'utf8', (err, htmlData) => {
28 | if (err) {
29 | console.error('err', err);
30 | return res.status(404).end()
31 | }
32 |
33 | const modules = [];
34 | const routerContext = {};
35 |
36 | // render the app as a string
37 | const html = ReactDOMServer.renderToString(
38 | modules.push(m)}>
39 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 |
47 | // get the stringified state
48 | const reduxState = JSON.stringify(store.getState());
49 |
50 | // map required assets to script tags
51 | const extraChunks = extractAssets(manifest, modules)
52 | .map(c => ``);
53 |
54 | // get HTML headers
55 | const helmet = Helmet.renderStatic();
56 |
57 | // now inject the rendered app into our html and send it to the client
58 | return res.send(
59 | htmlData
60 | // write the React app
61 | .replace('', `
${html}
`)
62 | // write the string version of our state
63 | .replace('__REDUX_STATE__={}', `__REDUX_STATE__=${reduxState}`)
64 | // append the extra js assets
65 | .replace('