├── .gitignore
├── .babelrc
├── static
└── data.json
├── Hello.scss
├── client.js
├── index.html
├── server.js
├── Hello.js
├── serverEntry.js
├── package.json
├── webpack.config.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | built
3 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015"]
3 | }
4 |
--------------------------------------------------------------------------------
/static/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "hello": "WORLD!"
3 | }
4 |
--------------------------------------------------------------------------------
/Hello.scss:
--------------------------------------------------------------------------------
1 | .root {
2 | font-size: 3em;
3 | font-family: sans-serif;
4 | color: rebeccapurple;
5 | }
6 |
--------------------------------------------------------------------------------
/client.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Transmit from 'react-transmit';
3 | import Hello from 'Hello';
4 |
5 | Transmit.render(
6 | Hello,
7 | {name: 'World'},
8 | document.getElementById('app')
9 | );
10 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hello {this.props.name}. Async hello {this.props.hello}
;
9 | }
10 | });
11 |
12 | export default Transmit.createContainer(Hello, {
13 | // These must be set, or else it would fail to render
14 | initialVariables: {},
15 | // each fragmen will be resolved into a prop
16 | fragments: {
17 | hello () {
18 | return fetch('http://localhost:3000/static/data.json')
19 | .then(r => r.json())
20 | .then(r => r.hello);
21 | }
22 | }
23 | });
24 |
--------------------------------------------------------------------------------
/serverEntry.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import React from 'react';
3 | import Transmit from 'react-transmit';
4 | import Hello from './Hello.js';
5 |
6 | function handleRender(req, res) {
7 | Transmit.renderToString(Hello, {name: 'World'})
8 | .then(({reactString, reactData}) => {
9 | fs.readFile('./index.html', 'utf8', function (err, file) {
10 | if (err) {
11 | return console.log(err);
12 | }
13 | const document = file.replace(/<\/div>/, `
${reactString}
`);
14 | const output = Transmit.injectIntoMarkup(document, reactData, ['/built/client.js']);
15 | res.send(output);
16 | });
17 |
18 | })
19 | .catch(e => console.log(e));
20 | }
21 |
22 | export default handleRender;
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server-side-rendering",
3 | "version": "0.0.1",
4 | "description": "The missing guide to server-side rendering",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "webpack && babel-node server.js"
8 | },
9 | "author": "Dmitri Pisarev",
10 | "license": "ISC",
11 | "dependencies": {
12 | "express": "^4.13.4",
13 | "isomorphic-fetch": "^2.2.1",
14 | "path": "^0.12.7",
15 | "react": "^0.14.8",
16 | "react-dom": "^0.14.8",
17 | "react-transmit": "^3.1.7"
18 | },
19 | "devDependencies": {
20 | "babel-cli": "^6.6.5",
21 | "babel-loader": "^6.2.4",
22 | "babel-preset-es2015": "^6.6.0",
23 | "babel-preset-react": "^6.5.0",
24 | "css-loader": "^0.23.1",
25 | "extract-text-webpack-plugin": "^1.0.1",
26 | "style-loader": "^0.13.1",
27 | "webpack": "^1.12.14",
28 | "webpack-node-externals": "^1.0.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
4 | const nodeExternals = require('webpack-node-externals');
5 |
6 | const config = {
7 | name: 'client',
8 | entry: ['client.js'],
9 | output: {
10 | path: path.join(__dirname, 'built/'),
11 | filename: 'client.js',
12 | publicPath: '/built/'
13 | },
14 | module: {
15 | loaders: [
16 | {
17 | test: /\.js$/,
18 | exclude: /node_modules/,
19 | loader: 'babel'
20 | },
21 | {
22 | test: /\.css$/,
23 | loader: ExtractTextPlugin.extract('style', 'css')
24 | },
25 | {
26 | test: /\.scss$/,
27 | loader: ExtractTextPlugin.extract('style', 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]')
28 | }
29 | ]
30 | },
31 | plugins: [new ExtractTextPlugin('styles.css')],
32 | resolve: {
33 | root: __dirname
34 | }
35 | };
36 |
37 | const serverConfig = {
38 | name: 'server',
39 | target: 'node',
40 | externals: [nodeExternals()],
41 | entry: [
42 | './serverEntry.js'
43 | ],
44 | output: {
45 | path: path.join(__dirname, 'built/'),
46 | filename: 'server.js',
47 | publicPath: 'built/',
48 | libraryTarget: 'commonjs2'
49 | },
50 | module: {
51 | loaders: [
52 | {
53 | test: /\.js$/,
54 | exclude: /node_modules/,
55 | loader: 'babel'
56 | },
57 | {
58 | test: /\.css$/,
59 | loader: 'null'
60 | },
61 | {
62 | test: /\.scss$/,
63 | loader: 'css-loader/locals?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]'
64 | }
65 | ]
66 | },
67 | resolve: {
68 | root: __dirname
69 | }
70 | };
71 |
72 | module.exports = [config, serverConfig];
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Interactive Guide to Server-side rendering with Webpack, React, React Transmit, CSS modules and more
2 |
3 | Follow the tutorial commit-by-commit, to see the server-side rendering drama unfold with a happy ending!
4 |
5 | [CLICK TO GET STARTED](https://github.com/dimaip/server-side-rendering/commits/master)
6 |
7 |
8 | ## Contents (mostly for Google)
9 |
10 | ### [Step 1: minimal Webpack, Babel and React setup](https://github.com/dimaip/server-side-rendering/commit/7d1d0677bca0ea94e820dc7e156c89e83ef823bb)
11 |
12 | RUN: `npm run start` and then open index.html in the browser.
13 |
14 | Webpack helps us to bundle our code with dependencies from npm
15 | (such as React), and then transforms the code with Babel, to make
16 | it compatible with ES5.
17 |
18 | ### [Step 2: trivial server-side rendering with Express](https://github.com/dimaip/server-side-rendering/commit/fad6b64e4ec3ee6b378c6ab0abd36f03fdba7c77)
19 |
20 | RUN: `npm run start` and go to `http://localhost:3000`.
21 |
22 | Now we are rendering the same Hello component both on client and server:
23 | with express server we pre-render the Hello component on the server, and
24 | server the client with rendered html, and with webpack we continue to
25 | bundle client.js into ES5 code that the browser would understand,
26 | just as we did at previous step.
27 |
28 | ### [Step 3: add styles](https://github.com/dimaip/server-side-rendering/commit/53d2ffa7bc9319722addced5bb94c5b231726a1b)
29 |
30 | Now lets learn to deal with styles. We configure webpack loaders to
31 | support loading CSS files. This is cool, but there comes one problem
32 | with server-side rendering: styles won't be loaded until all of JS loads,
33 | so no styles for devices without JS.
34 |
35 | Let's fix this problem with webpack's ExtractTextPlugin plugin: it
36 | extracts all CSS styles into one CSS file that we can serve to our client,
37 | so our page will instantly look perfectly styled, even without JS.
38 |
39 | ### [Step 3a: switch to CSS modules](https://github.com/dimaip/server-side-rendering/commit/e2c02444b1e7c6ec349511aa9b2da1a52aba5474)
40 |
41 | Everybody loves CSS modules, and the great news is that they come free with Webpack.
42 | The bad news is that we can't use them with server-side rendering, as we don't use
43 | Webpack when rendering on the server-side.
44 |
45 | So at this step we broke everything, and the only way to continue from here, is to
46 | start using Webpack to pre-build code for server-side rendering too, and that's
47 | what we'll do at the next step.
48 |
49 | ### [Step 3b: save the day by making webpack to render server-side code](https://github.com/dimaip/server-side-rendering/commit/6e36b9690816d414ca36775c6487e0b6dbd8abe3)
50 |
51 | To save our issue with CSS modules, we make Webpack to render both
52 | our client and our server side code. The best way to do it is to
53 | use Webpack's abillity to handle array of configs.
54 |
55 | With first config we transform our client-side code (`client.js`),
56 | just as we were doing before. But with the second config we transform
57 | `handleRender` function that we have extracted into `serverEntry.js`,
58 | so now our server-side rendering code gets processed by Webpack too.
59 | There we use css-loader/locals as a CSS loader, to just get class names from
60 | CSS modules, as that's all we need to render html on the server.
61 | Also notice how we use `target:node` and `nodeExternals`.
62 |
63 | Great! Now our build is fixed, so we can use CSS modules both during client
64 | and server rendering.
65 |
66 |
67 | ### [Step 4a: asyncronously fetching data](https://github.com/dimaip/server-side-rendering/commit/d9ce281b88d142cf52861223a201de6d47dfd428)
68 |
69 | Now let's fetch some data asyncronously. We'll use isomorphic-fetch,
70 | as it works both on client and server our of the box.
71 |
72 | Fetching data works just fine on the server, but the problem is that
73 | on the server we didn't wait for fetch to finish fetching the data
74 | before sending the response, so our pre-rendered html doesn't have any
75 | async data when it arrives.
76 | Let's try to fix it in the next step.
77 |
78 |
79 | ### [Step 4b: use react-transmit to declaratively define data deps](https://github.com/dimaip/server-side-rendering/commit/HEAD)
80 |
81 | There are multiple ways to solve async rendering issue. Here we'll
82 | use react-transmit to declaratively define our data dependencies per component,
83 | and return rendered html only when all data is resolved.
84 |
85 | It works even with nested components, constructing the single promises tree,
86 | which is qute cool. It is inspired by Facebook Relay, so if you are familiar
87 | with it, you'll feel right at home.
88 |
89 |
90 | ## That's all, folks!
91 |
92 | Got more tips or challenges with server-side rendering of React? Submit a PR!
93 |
--------------------------------------------------------------------------------