Building and Deploying React Applications Boris Nadion boris@astrails.com @borisnadion
@borisnadion boris@astrails.com
astrails http://astrails.com
2005
awesome web and mobile apps
building & deploying
building & deploying
facebookincubator/create-react-app Create React apps with no build configuration.
npm install -g create-react-app create-react-app my-app cd my-app/ npm start
customize like adding css preprocessors
yarn eject / npm eject
yarn eject / npm eject
not trivial
start from scratch
setup internals
environments ✓ development ✓ production x test
package.json
"babel": { 
 "presets": [ 
 [ 
 "es2015", 
 { 
 "modules": false 
 } 
 ], 
 "react", 
 "stage-0" 
 ], 
 "plugins": [ 
 "transform-runtime", 
 "babel-plugin-transform-class-properties", 
 "transform-object-rest-spread" 
 ], 
 "env": { 
 "production": { 
 "plugins": [ 
 "transform-react-inline-elements", 
 "transform-react-constant-elements", 
 "transform-react-remove-prop-types" 
 ] 
 } 
 } 
 }, 

babel-plugin-transform-class-properties class Bork { 
 //Property initializer syntax 
 instanceProperty = "bork"; 
 boundFunction = () => { 
 return this.instanceProperty; 
 } 
 
 //Static class properties 
 static staticProperty = "babelIsCool"; 
 static staticFunction = function() { 
 return Bork.staticProperty; 
 } 
 } 
 let n = { x, y, ...z }; 
 transform-object-rest-spread
transform-react-inline-elements babelHelpers.jsx(Baz, { foo: "bar" }, "1"); const Hr = () => { return <hr className="hr" />; }; 
 
 const _ref = <hr className="hr" />; 
 const Hr = () => { 
 return _ref; 
 }; 
 transform-react-inline-elements Baz.propTypes = {…} transform-react-remove-prop-types
“start": "env NODE_ENV=development webpack-dev-server --progress --colors", “build": "rimraf dist && env NODE_ENV=production webpack --colors && cp ./dist/* ../public/assets/",
webpack config
webpack config http://imgur.com/gallery/EnAmi
const ENV = process.env.NODE_ENV; 
 const VALID_ENVIRONMENTS = ['development', 'production']; 
 
 if (!VALID_ENVIRONMENTS.includes(ENV)) { 
 throw new Error(`${ ENV } is not valid environment!`); 
 } 
 const DEVELOPMENT_CONFIG = require('./config/webpack.dev'); 
 const PRODUCTION_CONFIG = require('./config/webpack.prod'); 
 
 const config = { 
 development: DEVELOPMENT_CONFIG, 
 production: PRODUCTION_CONFIG 
 }[ENV]; 
 
 
 const COMMON_CONFIG = { … };
 module.exports = webpackMerge.smart(COMMON_CONFIG, config); 

let’s look at the code webpack.config.js + dev + prod
3 bundles bundle.js + css client.js + css async.js + css
yarn start v0.24.4 $ env NODE_ENV=development webpack-dev-server --progress --colors 10% building modules 2/2 modules 0 active Project is running at http://0.0.0.0:9001/ webpack output is served from http://localhost:9001/assets/ 404s will fallback to /index.html Hash: 9a91c0c826ebb4c40f2a Version: webpack 2.6.1 Time: 6507ms Asset Size Chunks Chunk Names 0.js 806 bytes 0 [emitted] client.js 313 kB 1 [emitted] [big] client vendor.js 1.45 MB 2 [emitted] [big] vendor 0.js.map 572 bytes 0 [emitted] client.js.map 360 kB 1 [emitted] client index.html 421 bytes [emitted] webpack: Compiled successfully.
dev mode demo hot reload, async load, eslint errors
//… import { AppContainer } from 'react-hot-loader'; 
 
 //…
 const hotRender = () => { 
 render( 
 <AppContainer> 
 <Application store={ store } /> 
 </AppContainer>, 
 document.getElementById('root') 
 ); 
 }; 
 
 hotRender(); 
 
 module.hot.accept('components/Application', hotRender); 
 
 hot reload
import asyncComponent from 'components/AsyncComponent'; 
 
 const AsyncDashboard = asyncComponent(() => 
 import('./Dashboard').then(module => module.default) 
 ); 
 
 export default AsyncDashboard; 
 
 async load
import React from 'react'; 
 
 const asyncComponent = (getComponent) => 
 class AsyncComponent extends React.Component { 
 state = { Component: null }; 
 
 componentWillMount() { 
 if (!this.state.Component) { 
 getComponent().then(Component => { 
 this.setState({ Component }); 
 }); 
 } 
 } 
 render() { 
 const { Component } = this.state; 
 if (Component) { 
 return <Component { ...this.props } />; 
 } 
 return null; 
 } 
 }; 
 
 export default asyncComponent; 
 async load
prod build yarn build
yarn build v0.24.4 $ rimraf dist && env NODE_ENV=production webpack --colors && cp ./dist/* ../public/assets/ Hash: f5404348a5a4eadca2c5 Version: webpack 2.6.1 Time: 9894ms Asset Size Chunks Chunk Names client-149e7f81934ccd4797d6.bundle.js.map 183 kB 1 [emitted] client 0-b065752a37e19efffbe1.bundle.js 318 bytes 0 [emitted] webpack-chunk-manifest.json 79 bytes [emitted] vendor-411f8db22ac4a264ff0d.bundle.js 265 kB 2 [emitted] [big] vendor client-e9da9d78d42878a4c3a5a7ab1330ea79.css 2.7 kB 1 [emitted] client 0-b065752a37e19efffbe1.bundle.js.map 2.11 kB 0 [emitted] client-149e7f81934ccd4797d6.bundle.js 24 kB 1 [emitted] client client-e9da9d78d42878a4c3a5a7ab1330ea79.css.map 120 bytes 1 [emitted] client client-149e7f81934ccd4797d6.bundle.js.gz 8.35 kB [emitted] vendor-411f8db22ac4a264ff0d.bundle.js.gz 77.9 kB [emitted] index.html 493 bytes [emitted] webpack-asset-manifest.json 468 bytes [emitted]
new webpack.HashedModuleIdsPlugin(), new ManifestPlugin({ 
 fileName: 'webpack-asset-manifest.json' 
 }), 
 
 new ChunkManifestPlugin({ 
 filename: 'webpack-chunk-manifest.json', 
 manifestVariable: 'webpackManifest' 
 }), 

// webpack-chunk-manifest.json {"0":"0-b065752a37e19efffbe1.bundle.js"} 
 // webpack-asset-manifest.json
 { 
 "0-b065752a37e19efffbe1.bundle.js": "0-b065752a37e19efffbe1.bundle.js", 
 "0-b065752a37e19efffbe1.bundle.js.map": "0-b065752a37e19efffbe1.bundle.js.map", 
 "client.css": "client-e9da9d78d42878a4c3a5a7ab1330ea79.css", 
 "client.css.map": "client-e9da9d78d42878a4c3a5a7ab1330ea79.css.map", 
 "client.js": "client-149e7f81934ccd4797d6.bundle.js", 
 "client.js.map": "client-149e7f81934ccd4797d6.bundle.js.map", 
 "vendor.js": "vendor-411f8db22ac4a264ff0d.bundle.js" 
 } 

4 bundles vendor.js client.js 0.js client.css
html from server /* … */ <script type=“text/javascript”> window.apiEndPoint = "http://stage.example.com" </script> <link href="//xxx/client.css" rel="stylesheet" /> <script src="//xxx/vendor.js”></script> <script src="//xxx/client.js"></script> /* … */ } xxx=?
<div id="root"></div> <%= api_endpoint_from_environment %> <%= client_application_stylesheet_tag 'client.css' %>
 <%= client_application_javascript_tag 'vendor.js' %> 
 <%= client_application_javascript_tag 'client.js' %> 
 server template
module ClientApplicationHelper # client_application_javascript_tag 'client.js'
 def client_application_javascript_tag(bundle) 
 src = 
 if client_application[:use_manifest] # "client.js": "client-149e7f81934ccd4797d6.bundle.js", 
 manifest = client_application[:asset_manifest][bundle] 
 # static asset
 "/assets/#{bundle}" 
 else # dev mode
 "http://localhost:9001/assets/#{bundle}" 
 end 
 
 javascript_include_tag(src)
 end 
 
 def client_application_stylesheet_tag(bundle) # … # almost the same but no need to render in dev mode
 end 
 end 
 

serve from • webpack dev server (for dev mode) • same server, static assets • static assets through CDN • CDN direct • whatever
awesome
almost awesome
<%= client_application_stylesheet_tag 'client.css' %> <%= client_application_javascript_tag 'vendor.js' %> 
 <%= client_application_javascript_tag 'client.js' %> 
 in a context of a request
current_user req.current_user, request.user[. is_authenticated], …
module ClientApplicationHelper 
 def client_application_javascript_tag(bundle) 
 src = 
 if client_application[:use_manifest] # "client.js": "client-149e7f81934ccd4797d6.bundle.js", 
 manifest = assets_manifest_for(current_user)[bundle]
 # …
 end 
 
 javascript_include_tag(src)
 end 
 end 
 

storing manifests per user S3, database, redis, memcache, etc + default manifest for the rest of the users
assets_manifest_for(current_user)[bundle] • A/B testing • features testing in production env • UI experiments • gradually rolling out new features
assets_manifest_for(current_user)[bundle] bundles v1.12 default bundles v1.13 debugging an issue bundles v2.0 testing new release user with a bug in v1.12 marketing user all users
separate server and client deployments
client lifecycle • build: get new bundles + manifest • deploy: upload bundles to remote storage (S3) + warm up CDN • release: update user’s or default manifest
awesome
almost awesome
http://www.enjoyart.com/single_posters/animals_art_photo/NoahsArkTakinoAnimalsArtPrintPoster.htm zoo
bundles v2.0bundles v2.1bundles v2.2bundles v2.3bundles v2.4 server compatibility
API compatibility
develop deploy test release new frontend version new backend version local staging production
release = update default manifest for all the users
server first server is always backward compatible easier to maintain compatibility on server with API versioning
zoo = not an engineering issue but administrative one
awesome
really awesome
https://github.com/astrails/rails_react_webpack thanks to mike@astrails.com aka @mihap
http://astrails.com/blog slides will be available
thanks! Boris Nadion http://astrails.com boris@astrails.com @borisnadion

Building and deploying React applications