├── .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 | Server-side rendering tutorial 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | import handleRender from './built/server.js'; 4 | 5 | const app = express(); 6 | // Serve built files with express static files middleware 7 | app.use('/built', express.static(path.join(__dirname, 'built'))); 8 | // Serve normal requests with our handleRender function 9 | app.use('/static', express.static(path.join(__dirname, 'static'))); 10 | app.get('*', handleRender); 11 | app.listen(3000); 12 | console.log('=== Go to http://localhost:3000 ==='); 13 | -------------------------------------------------------------------------------- /Hello.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Transmit from 'react-transmit'; 3 | import fetch from 'isomorphic-fetch'; 4 | import s from 'Hello.scss'; 5 | 6 | const Hello = React.createClass({ 7 | render: function() { 8 | return
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 | --------------------------------------------------------------------------------