├── .gitignore ├── .babelrc ├── src ├── App.js ├── client.js ├── index.js └── server.js ├── package.json ├── webpack.config.js ├── webpack.server.config.js ├── dev.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | public/main.js 3 | webpack-assets.json 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: [ 3 | 'env', 4 | 'react' 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default () => 4 |

hello

5 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { hydrate } from 'react-dom' 3 | import App from './App' 4 | 5 | hydrate( 6 | , 7 | document.getElementById('root') 8 | ) 9 | 10 | if (module.hot) module.hot.accept() 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import app from './server' 3 | 4 | const server = http.createServer(app) 5 | 6 | let current = app 7 | 8 | server.listen(3000, err => { 9 | if (err) console.log(err) 10 | console.log('server listening on port 3000') 11 | }) 12 | 13 | if (module.hot) { 14 | module.hot.accept('./server.js', () => { 15 | console.log('server reload') 16 | server.removeListener('request', current) 17 | const next = require('./server').default 18 | server.on('request', next) 19 | current = next 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { renderToString } from 'react-dom/server' 3 | import express from 'express' 4 | import App from './App' 5 | 6 | const assets = require('../webpack-assets.json') 7 | 8 | const app = (req, res) => { 9 | const body = renderToString( 10 | 11 | ) 12 | 13 | const html = ` 14 | hello 15 |
${body}
16 | 17 | ` 18 | 19 | res.send(html) 20 | } 21 | 22 | const server = express() 23 | 24 | server.use(express.static('dist/public')) 25 | server.use(app) 26 | 27 | export default server 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "react-server", 4 | "scripts": { 5 | "start": "node dist/server.js", 6 | "build": "webpack && webpack --config webpack.server.config.js", 7 | "dev": "node dev.js" 8 | }, 9 | "author": "Brent Jackson", 10 | "dependencies": { 11 | "@koa/cors": "^2.2.2", 12 | "assets-webpack-plugin": "^3.9.5", 13 | "babel-core": "^6.26.3", 14 | "babel-loader": "^7.1.5", 15 | "babel-preset-env": "^1.7.0", 16 | "babel-preset-react": "^6.24.1", 17 | "chalk": "^2.4.1", 18 | "express": "^4.16.3", 19 | "fs-extra": "^7.0.0", 20 | "koa": "^2.5.2", 21 | "koa-webpack": "^5.1.0", 22 | "progress-bar-webpack-plugin": "^1.11.0", 23 | "react": "^16.4.2", 24 | "react-dom": "^16.4.2", 25 | "start-server-webpack-plugin": "^2.2.5", 26 | "webpack": "^4.16.5", 27 | "webpack-command": "^0.4.1", 28 | "webpack-node-externals": "^1.7.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const ProgressBarPlugin = require('progress-bar-webpack-plugin') 3 | const AssetsPlugin = require('assets-webpack-plugin') 4 | const chalk = require('chalk') 5 | 6 | const isDev = process.env.NODE_ENV === 'development' 7 | 8 | const rules = [ 9 | { 10 | test: /\.js$/, 11 | exclude: /node_modules/, 12 | use: 'babel-loader' 13 | } 14 | ] 15 | 16 | const progress = new ProgressBarPlugin({ 17 | width: '24', 18 | complete: '█', 19 | incomplete: chalk.gray('░'), 20 | format: [ 21 | chalk.blue('[client] :bar'), 22 | chalk.blue(':percent'), 23 | chalk.gray(':elapseds :msg'), 24 | ].join(' '), 25 | summary: false, 26 | customSummary: () => {}, 27 | }) 28 | 29 | module.exports = { 30 | stats: 'errors-only', 31 | mode: isDev ? 'development' : 'production', 32 | entry: [ 33 | path.resolve('src/client.js') 34 | ], 35 | output: { 36 | path: path.resolve('dist/public'), 37 | filename: 'main.js', 38 | publicPath: isDev ? 'http://localhost:3001/' : '/' 39 | }, 40 | module: { 41 | rules 42 | }, 43 | plugins: [ 44 | progress, 45 | new AssetsPlugin() 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /webpack.server.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const nodeExternals = require('webpack-node-externals') 3 | const ProgressBarPlugin = require('progress-bar-webpack-plugin') 4 | const chalk = require('chalk') 5 | const base = require('./webpack.config') 6 | 7 | const isDev = process.env.NODE_ENV === 'development' 8 | 9 | module.exports = Object.assign({}, base, { 10 | target: 'node', 11 | externals: [ 12 | nodeExternals({ 13 | whitelist: [ 14 | 'webpack/hot/poll?300' 15 | ] 16 | }) 17 | ], 18 | entry: [ 19 | isDev ? 'webpack/hot/poll?300' : null, 20 | path.resolve('src/index.js') 21 | ].filter(Boolean), 22 | output: { 23 | libraryTarget: 'umd', 24 | path: path.resolve('dist'), 25 | filename: 'server.js', 26 | publicPath: '/' 27 | }, 28 | plugins: [ 29 | new ProgressBarPlugin({ 30 | width: '24', 31 | complete: '█', 32 | incomplete: chalk.gray('░'), 33 | format: [ 34 | chalk.magenta('[server] :bar'), 35 | chalk.magenta(':percent'), 36 | chalk.gray(':elapseds :msg'), 37 | ].join(' '), 38 | summary: false, 39 | customSummary: () => {}, 40 | }) 41 | ] 42 | }) 43 | -------------------------------------------------------------------------------- /dev.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'development' 2 | const path = require('path') 3 | const fs = require('fs-extra') 4 | const Koa = require('koa') 5 | const koaWebpack = require('koa-webpack') 6 | const cors = require('@koa/cors') 7 | const webpack = require('webpack') 8 | const StartServerPlugin = require('start-server-webpack-plugin') 9 | 10 | const app = new Koa() 11 | 12 | const rules = [ 13 | { 14 | test: /\.js$/, 15 | exclude: /node_modules/, 16 | use: 'babel-loader' 17 | } 18 | ] 19 | 20 | const config = { 21 | client: require('./webpack.config'), 22 | server: require('./webpack.server.config') 23 | } 24 | 25 | config.server.plugins.push( 26 | new webpack.HotModuleReplacementPlugin(), 27 | new webpack.WatchIgnorePlugin([ 28 | path.resolve('webpack-assets.json') 29 | ]), 30 | new StartServerPlugin({ 31 | name: 'server.js' 32 | }) 33 | ) 34 | 35 | const compiler = webpack(config.client) 36 | const serverCompiler = webpack(config.server) 37 | 38 | compiler.hooks.done.tap('dev-server', () => { 39 | serverCompiler.watch({}, stats => {}) 40 | }) 41 | 42 | const start = async () => { 43 | fs.removeSync('./webpack-assets.json') 44 | 45 | const middleware = await koaWebpack({ 46 | compiler, 47 | hotClient: { 48 | logLevel: 'error' 49 | }, 50 | devMiddleware: { 51 | logLevel: 'error', 52 | stats: 'errors-only', 53 | } 54 | }) 55 | app.use(cors()) 56 | app.use(middleware) 57 | const server = app.listen(3001) 58 | } 59 | 60 | start() 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # React Server 3 | 4 | Univeral (isomorphic) React express app with webpack and hot module replacement 5 | 6 | **Note:** This is intended for demonstration and educational purposes. 7 | If you're building a server-rendered React application, I'd recommend looking at [Next.js][] or [Razzle][], which was the inspiration for this. 8 | 9 | - Universal app with SSR and client side rehydration 10 | - Hot module replacement on both server and client 11 | - Support for ES6 & JSX with [Babel][] 12 | - Minimal boilerplate with no other webpack loaders or configuration 13 | 14 | 15 | ## Development 16 | 17 | ```sh 18 | npm i 19 | npm run dev 20 | ``` 21 | 22 | ## Production 23 | 24 | ```sh 25 | npm run build && npm start 26 | ``` 27 | 28 | ## How development mode works 29 | 30 | In much the same way [Razzle][] works, development mode runs two webpack compilers. 31 | The client compiler is passed to [koa-webpack][] to start a development server on port `3001`. 32 | Once the client compiler has finished, the server compiler is put into watch mode. 33 | Using the [start-server-webpack-plugin][], the server starts running on port `3000`. 34 | Opening `http://localhost:3000` in a browser will make a request from the server and get `http://localhost:3001/main.js` for the client side app, which will rehydrate the server side HTML. 35 | Both the server and the client use `App.js` for rendering the body. 36 | 37 | When any of the source files change, webpack's hot module replacement will update both the server and client bundles. 38 | 39 | ### Credits 40 | 41 | Lots of ideas stolen directly from [Razzle][] 42 | 43 | [Razzle]: https://github.com/jaredpalmer/razzle 44 | [Next.js]: https://github.com/zeit/next.js/ 45 | [Babel]: https://github.com/babel/babel 46 | [koa-webpack]: https://github.com/shellscape/koa-webpack 47 | [start-server-webpack-plugin]: https://github.com/ericclemmons/start-server-webpack-plugin 48 | 49 | MIT License 50 | --------------------------------------------------------------------------------