Skip to content
This repository was archived by the owner on Dec 29, 2020. It is now read-only.

Commit ae3dc7e

Browse files
author
destructobeam
committed
added emotion with SSR, and loadable components
1 parent c9648ce commit ae3dc7e

17 files changed

+537
-85
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Very barebones starter kit for server rendering React applications, probably
44
could be easily used for any other JavaScript framework that can render to
55
string.
66

7+
Includes Webpack hot reloading for server and client apps.
8+
79
## Client
810

911
- React
@@ -17,8 +19,6 @@ series of steps required to render your application on the server.
1719

1820
## TODO
1921

20-
- Add React Router middleware
21-
- Check React Router's status code and apply to Koa
22-
- Add in Emotion CSS-in-JS
23-
- Code splitting with loadable-components
2422
- Figure out and implement client only code splitting for when you need super fast TTI, and don't care about overly complex auxiliary interfaces being rendered on the server and included in the initially loaded bundle.
23+
- Possibly use streaming response on server
24+
- Production settings

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
"start": "NODE_ENV=production node dist/server.js"
1111
},
1212
"dependencies": {
13+
"@loadable/component": "^5.2.2",
14+
"@loadable/server": "^5.2.2",
15+
"emotion": "^10.0.5",
16+
"emotion-server": "^10.0.5",
1317
"koa": "^2.6.2",
1418
"koa-static": "^5.0.0",
1519
"react": "^16.6.3",
@@ -24,11 +28,13 @@
2428
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
2529
"@babel/preset-env": "^7.2.0",
2630
"@babel/preset-react": "^7.0.0",
31+
"@loadable/babel-plugin": "^5.2.2",
32+
"@loadable/webpack-plugin": "^5.2.2",
2733
"babel-loader": "^8.0.4",
34+
"babel-plugin-emotion": "^10.0.5",
2835
"start-server-webpack-plugin": "^2.2.5",
2936
"webpack": "^4.27.1",
3037
"webpack-dev-server": "^3.1.10",
31-
"webpack-manifest-plugin": "^2.0.4",
3238
"webpack-merge": "^4.1.5",
3339
"webpack-node-externals": "^1.7.2",
3440
"webpackbar": "^3.1.4"

scripts/serve.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
const http = require('http');
2-
const app = require('../src/server').default;
2+
const https = require('https');
33

4+
const app = require('../src/server').default;
5+
const dev = process.env.NODE_ENV !== 'production';
46
const port = process.env.PORT || 3000;
57

68
let handler = app.callback();
7-
const server = http.createServer(handler);
9+
let server;
10+
11+
if (dev) {
12+
server = http.createServer(handler);
13+
} else {
14+
server = https.createServer(handler);
15+
}
816

917
server.listen(port, error => {
1018
if (error) {

src/app.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
import React from 'react';
2+
import { Helmet } from 'react-helmet';
23

3-
const App = () => <h1>SSR Starter</h1>;
4+
const App = () => (
5+
<>
6+
<Helmet>
7+
<html lang="en" />
8+
<meta charSet="utf-8" />
9+
<title>React SSR Starter</title>
10+
</Helmet>
11+
12+
<h1>React SSR Starter</h1>
13+
</>
14+
);
415

516
export default App;

src/client.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import React from 'react';
2+
import { BrowserRouter as Router } from 'react-router-dom';
3+
import { loadableReady } from '@loadable/component';
24
import { hydrate } from 'react-dom';
35

46
import App from './app';
57

6-
const Client = () => <App />;
8+
const Client = () => (
9+
<Router>
10+
<App />
11+
</Router>
12+
);
713

8-
hydrate(<Client />, document.getElementById('main'));
14+
loadableReady(() => {
15+
hydrate(<App />, document.getElementById('main'));
16+
})
917

1018
if (module.hot) {
1119
module.hot.accept();

src/server.js

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,49 @@
1+
import path from 'path';
2+
13
import Koa from 'koa';
24
import KoaStatic from 'koa-static';
35

46
import bootstrap from './server/bootstrap';
57
import react from './server/react';
6-
import status from './server/status';
8+
import emotion from './server/emotion';
9+
import helmet from './server/helmet';
10+
import redirect from './server/redirect';
11+
import loadable from './server/loadable';
712
import render from './server/render';
813

914
const dev = process.env.NODE_ENV !== 'production';
1015
const server = new Koa();
1116

1217
if (!dev) {
1318
// Use webpack-dev-server in dev mode, otherwise serve static assets normally
14-
server.use(KoaStatic('public'));
19+
server.use(KoaStatic(path.resolve('..', 'public')));
1520
}
1621

1722
// Load asset manifest in to state and watch changes to manifest in development
1823
server.use(bootstrap);
24+
25+
// Set up chunk extractor
26+
server.use(loadable);
27+
28+
// Check React Router status code and redirect if needed
29+
server.use(redirect);
30+
1931
// Create React app string
2032
server.use(react);
21-
// Check React Router status code and redirect if needed
22-
server.use(status);
33+
34+
// Create stylesheets
35+
server.use(emotion);
36+
37+
// Set up content for head
38+
server.use(helmet);
39+
2340
// Generate (and set as body) HTML response
2441
server.use(render);
2542

43+
// Log errors
2644
server.on('error', (error, context) => {
27-
console.log(`Server error: ${error}`);
28-
console.log(`Error context: ${context}`);
45+
console.error(`Server error: ${error}`);
46+
console.error(`Error context: ${context}`);
2947
});
3048

3149
export default server;

src/server/bootstrap.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import path from 'path';
44
const dev = process.env.NODE_ENV !== 'production';
55

66
const bootstrap = async (context, next) => {
7-
console.log('Bootstrap called');
7+
console.log('Bootstrap down');
88

99
context.state.assetManifest = require('../../public/assets/manifest.json');
1010

1111
if (dev) {
1212
fs.watch('public/assets/manifest.json', (eventType, filename) => {
1313
if (eventType === 'change') {
14-
const manifestFile = fs.readFile('public/assets/manifest.json')
14+
const manifestFile = fs.readFile('public/assets/manifest.json');
1515

1616
try {
1717
context.state.assetManifest = JSON.parse(manifestFile);
@@ -23,6 +23,8 @@ const bootstrap = async (context, next) => {
2323
}
2424

2525
await next();
26+
27+
console.log('Bootstrap up');
2628
};
2729

2830
export default bootstrap;

src/server/emotion.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { renderStylesToString } from 'emotion-server';
2+
3+
const emotion = async (context, next) => {
4+
console.log('Emotion down');
5+
6+
context.state.reactString = renderStylesToString(context.state.reactString);
7+
8+
await next();
9+
10+
console.log('Emotion up');
11+
};
12+
13+
export default emotion;

src/server/helmet.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Helmet } from 'react-helmet';
2+
3+
const helmet = async (context, next) => {
4+
console.log('Helmet down');
5+
6+
context.state.helmet = Helmet.renderStatic();
7+
8+
await next();
9+
10+
console.log('Helmet up');
11+
};
12+
13+
export default helmet;

src/server/loadable.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import { ChunkExtractor } from '@loadable/server';
4+
5+
const loadable = async (context, next) => {
6+
console.log('Loadable down');
7+
8+
const statsFile = path.resolve('public', 'assets', 'loadable-stats.json');
9+
10+
context.state.chunkExtractor = new ChunkExtractor({ statsFile });
11+
12+
await next();
13+
14+
console.log('Loadable up');
15+
};
16+
17+
export default loadable;

src/server/react.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
11
import React from 'react';
22
import { StaticRouter as Router } from 'react-router';
3+
import { ChunkExtractorManager } from '@loadable/server';
34
import { renderToString } from 'react-dom/server';
45

56
import App from '../app';
67

78
const react = async (context, next) => {
8-
console.log('react called');
9+
console.log('React down');
910

10-
context.state.reactString = renderToString(<App />);
11+
const {
12+
state: { chunkExtractor, reactRouterContext },
13+
url,
14+
} = context;
15+
16+
context.state.reactString = renderToString(
17+
<ChunkExtractorManager>
18+
<Router context={reactRouterContext} location={url}>
19+
<App />
20+
</Router>
21+
</ChunkExtractorManager>,
22+
);
1123

1224
await next();
25+
26+
console.log('React up');
1327
};
1428

1529
export default react;

src/server/redirect.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const redirect = async (context, next) => {
2+
console.log('Redirect down');
3+
4+
context.state.reactRouterContext = {};
5+
6+
await next();
7+
8+
const { reactRouterContext } = context.state;
9+
10+
console.log('Redirect up', reactRouterContext);
11+
12+
if (reactRouterContext.url) {
13+
context.redirect(reactRouterContext.url);
14+
}
15+
};
16+
17+
export default redirect;

src/server/render.js

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
11
const render = async (context, next) => {
2-
console.log('Render called');
2+
await next();
3+
4+
console.log('Render up');
5+
6+
const {
7+
state: {
8+
assetManifest,
9+
chunkExtractor,
10+
helmet: { bodyAttributes, htmlAttributes, link, meta, title },
11+
reactString,
12+
},
13+
} = context;
314

415
context.body = `
5-
<html>
16+
<!doctype html>
17+
<html ${htmlAttributes.toString()}>
618
<head>
719
<title>SSR Starter</title>
20+
${title.toString()}
21+
${meta.toString()}
22+
${link.toString()}
823
</head>
9-
<body>
10-
<div id="main" role="main">${context.state.reactString}</div>
11-
<script src="${context.state.assetManifest["bundle.js"]}"></script>
24+
<body ${bodyAttributes.toString()}>
25+
<div id="main" role="main">${reactString}</div>
26+
${chunkExtractor.getScriptTags()}
1227
</body>
1328
</html>
14-
`
29+
`;
1530
};
1631

1732
export default render;

src/server/status.js

Lines changed: 0 additions & 6 deletions
This file was deleted.

0 commit comments

Comments
 (0)