Universal React with Rails: Part III
Building Universal app
It’s been awhile since I wrote my previous post about Rails API. Meantime a lot of things happened in JavaScript world: ES6 was officially approved and renamed to ES2015, “isomorphic” javascript (almost) became “universal”, one of the flux frameworks (which I’ve started to use in my apps) was deprecated in favour of new one (which we will use in the next post).
There is a great quote by Addy Osmani:
First do it, then do it right, then do it better.
Our goal is to learn how to build modern javascript apps and efficiently manage their state. But before do complex things, we should learn basics. So our starting point will be super simple app, which doesn’t interact with API and have only static data inside React components. In other words we will build static javascript application with server rendering using React.
:before
Here is the list of stuff you need to be familiar with to feel comfortable while reading this post:
- Basic React components
- React-router 1.0.0 API
- ES6 (ES2015) syntax (deeper).
- Express 4.x API
- Gulp 3.x API
- Webpack API
- Jade API
Also take a look at these posts, we will use them later.
- How to avoid
../../../../this/crazy/requires
in Node.js - How to handle long-term caching of static assets with Webpack
You can return to these links anytime, if you’ll stuck somewhere down the road.
The app
We gonna build small site with 2 pages: Main and About. Nothing special, except that it will be universal.
Project structure
I always keep in mind that there can be multiple bundles in app (public site and admin area for example), so some modules will be split on two logical parts:
- reusable abstraction (it will be shared between all bundles)
- bundle-specific stuff.
This is how it usually looks like in my projects.
|-- /app <---- App container
|------ /assets <---- Shared assets (css, fonts etc.)
|------ /bundles <---- Bundles
|---------- /[bundle 1]
|---------- /[bundle 2]
|------ /errors <---- Server errors layouts
|------ /libs <---- Shared libs
|
|-- /build <---- Build configs
|-- /config <---- Application configs
|-- /public <---- Public assets
|-- /server <---- Express middlewares
|-- .dotfiles <---- Dotfiles
|-- gulpfile.babel.js <---- Gulpfile
|-- package.json <---- You know what it is
|-- server.[bundle].js <---- Bundle's Express server
|-- server.dev.js <---- Webpack dev server
|-- server.js <---- Reusable part for Express servers
Shared code base
Let’s start with the shared code, which will be the same on the server and on the client. Actually, it’s super simple React components with super simple react-router setup.
First of all we’ll define routes:
import React from 'react'; | |
import { Route } from 'react-router'; | |
import App from '../layouts/Layout'; | |
import Main from '../components/Main'; | |
import About from '../components/About'; | |
import NotFound from '../components/NotFound'; | |
export default ( | |
<Route name="app" component={App}> | |
<Route name="main" path="/" component={Main} /> | |
<Route name="about" path="/about" component={About} /> | |
<Route name="not-found" path="*" component={NotFound} /> | |
</Route> | |
); |
And this is Layout component:
/* app/bundles/app/layouts/Layout.jsx */ | |
import React from 'react'; | |
import Header from '../components/Header'; | |
export default class Layout extends React.Component { | |
constructor(props, context) { | |
super(props, context); | |
} | |
render() { | |
return ( | |
<section id="layout"> | |
<Header /> | |
{/* In react-router 1.0.0 there is no `RouteHandler` */} | |
{/* It's just `this.props.children` instead */} | |
{this.props.children} | |
</section> | |
); | |
} | |
} |
Main & About components are trivial — you can find them here.
Our main goal — make it universal. Let’s do it.
Server side rendering
When request from the browser gets to our app, it will be handled by Express — Node.js web app framework. Express server doesn’t know anything about React, it just takes request, parses it and passes it with response object to middleware function. This middleware takes care of the rest: initializes React app, renders initial html and sends it to browser (by response object from Express) with client’s javascript app on the board.
Express server:
/* server.app.js */ | |
// This is entry point of our `app` bundle. | |
// And here we collect bundle specific stuff | |
// to pass it to reusable abstraction part. | |
// We can add `server.admin.js` bundle etc. | |
// We gonna use ES6 / ES7 syntax in our modules | |
// So we need to transform it to ES5 with every `require` from here on | |
require('babel/register')({ | |
extensions: ['.js', '.jsx'], | |
stage : 0 | |
}); | |
// Middleware function of `app` bundle | |
var initter = require('./app/bundles/app/initters/server'); | |
// Bundle settings | |
var config = require('./config/server.app'); | |
// Starting express server | |
require('./server')(initter, config); |
/* server.js */ | |
// This is shared between all bundles | |
import express from 'express'; | |
import parser from 'body-parser'; | |
import cookies from 'cookie-parser'; | |
import path from 'path'; | |
export default (initter, config) => { | |
// Defining some globals | |
global.__CLIENT__ = false; | |
global.__SERVER__ = true; | |
global.__DEV__ = config.env !== 'production'; | |
// Initializing app | |
const app = express(); | |
// Parsing json bodies | |
app.use(parser.json()); | |
// Parsing urlencoded bodies | |
app.use(parser.urlencoded({ extended: true })); | |
// Parsing cookies | |
app.use(cookies()); | |
// Serving static files from `public` dir | |
app.use(express.static(path.join(__dirname, 'public'))); | |
// Here we are! | |
// Transferring incoming requests to middleware function of the bundle | |
app.use('/', initter); | |
// Setting up port to listen | |
app.set('port', config.appPort); | |
// And listening it | |
app.listen(app.get('port'), function() { | |
console.log(`=> 🚀 Express ${config.bundle} ${config.env} server is running on port ${this.address().port}`); | |
}); | |
} |
Config for app bundle:
/* config/server.app.js */ | |
// Importing shared config part | |
import config from './server'; | |
// Defining name of the bundle | |
config.bundle = 'app'; | |
// Defining port of bundle | |
// In production we will store this in APP_PORT environment variable | |
// In development it will be 3500 | |
config.appPort = process.env.APP_PORT || 3500; | |
export default config; |
/* config/server.js */ | |
// This is shared between all bundles | |
let config = {}; | |
// Defining environment | |
config.env = process.env.NODE_ENV || 'development'; | |
// Defining port for webpack dev server (details below) | |
config.devPort = 3001; | |
export default config; |
Express did all the routine job and passed request to bundle’s middleware (I call it initter), where all magic is gonna happen.
Middleware function in Express is function with following signature:
function middleware(req, res, next) {
res.send("Hello, world!")
}
This is entry point of React app, and it also will be split on abstraction / bundle-specific parts:
/* app/bundles/app/initters/server.jsx */ | |
// Here we collect bundle specific stuff | |
// to pass it to shared initter with bundle `params` | |
import initter from 'app/libs/initters/server'; | |
import getAsset from 'app/libs/getAsset'; | |
import config from 'config/server.app'; | |
import routes from '../routes/routes'; | |
import Head from '../layouts/Head'; | |
export default (req, res, next) => { | |
const { bundle } = config; | |
const params = { | |
// Name of the bundle | |
bundle, | |
// Routes | |
routes, | |
// <head> template | |
Head, | |
// Variables for Jade template | |
// Here we store paths to compiled assets to inject them in html | |
// Explore `getAsset` helper in `app/libs` folder | |
locals: { | |
jsAsset : getAsset(bundle, 'js'), | |
cssAsset : getAsset(bundle, 'css'), | |
vendorAsset: getAsset('vendor', 'js') | |
} | |
}; | |
// Initializing app | |
initter(req, res, next, params); | |
} |
/* app/libs/initters/server.jsx */ | |
// This initter is shared between all bundles | |
import React from 'react'; | |
import Router from 'react-router'; | |
import Location from 'react-router/lib/Location'; | |
import serialize from 'serialize-javascript'; | |
import jade from 'jade'; | |
export default (req, res, next, params) => { | |
// Storing params in variables | |
const { routes, bundle, locals, Head } = params; | |
// Creating location object for the server router | |
const location = new Location(req.path, req.query); | |
// Running the router | |
Router.run(routes, location, (error, initialState, transition) => { | |
// If something went wrong, responding with 500 | |
if (error) return res.status(500).send(error); | |
try { | |
// Rendering <head> tag | |
// Using `renderToStaticMarkup` here, | |
// because we don't need React ids on these nodes | |
locals.head = React.renderToStaticMarkup( | |
<Head cssAsset={locals.cssAsset} /> | |
); | |
// Rendering app | |
locals.body = React.renderToString( | |
<Router location={location} {...initialState} /> | |
); | |
// Storing webpack chunks in variable | |
// to expose it as global var in html for production bundles | |
// It's related to long-term caching of assets (details below) | |
const chunks = __DEV__ ? {} : require('public/assets/chunk-manifest.json'); | |
locals.chunks = serialize(chunks); | |
// Defining path to jade layout | |
const layout = `${process.cwd()}/app/bundles/${bundle}/layouts/Layout.jade`; | |
// Compiling initial html | |
const html = jade.compileFile(layout, { pretty: false })(locals); | |
// 😽💨 | |
res.send(html); | |
} catch (err) { | |
// If something went wrong, responding with 500 | |
res.status(500).send(err.stack); | |
} | |
}); | |
} |
Jade template is pretty straightforward:
doctype html | |
html | |
// Injecting `locals.head` | |
!= head | |
body | |
// Injecting `locals.body` | |
#app!= body | |
// Injecting `locals.chunks` global for webpack | |
script. | |
window.__CHUNKS__ = !{chunks}; | |
// Injecting js assets | |
script(src="#{vendorAsset}") | |
script(src="#{jsAsset}") |
At this point initial html was rendered and sent to browser with client’s javascript app.
Client entry point
Client’s initter is much easier.
/* app/bundles/app/initters/client.jsx */ | |
// Here we collect bundle specific stuff, same as for server | |
// Babel polyfill, required for some of the ES6/ES7 features | |
import polyfill from 'babel/polyfill'; | |
import initter from 'app/libs/initters/client'; | |
import routes from '../routes/routes'; | |
const params = { routes }; | |
export default initter(params); |
/* app/libs/initters/client.jsx */ | |
// This initter is shared between all bundles | |
import React from 'react'; | |
import Router from 'react-router'; | |
import BrowserHistory from 'react-router/lib/BrowserHistory'; | |
export default (params) => { | |
const { routes } = params; | |
// Creating history object for the client router | |
const history = new BrowserHistory(); | |
// Creating app container | |
const AppContainer = ( | |
<Router history={history} children={routes} /> | |
); | |
// Selecting DOM container for app | |
const appDOMNode = document.getElementById('app'); | |
// Flushing application to DOM | |
React.render(AppContainer, appDOMNode); | |
} |
Next thing we need to learn is how to build the client bundles and how to setup production / development environments with gulp & webpack.
Gulp, Webpack & hot reloading
Building process is the biggest pain in the ass in javascript world, so get ready. We’re gonna setup two different environments: development & production. In the end there will be available 3 console commands:
npm start or gulp start:dev
Runs development web server with hot reloading.
npm run prod or gulp start:prod
Runs local server with production ready bundles to check how it works.
npm run build or just gulp
Compiles production builds.
Key tools we’re gonna use are Gulp and Webpack. Here is the gulpfile:
/* gulpfile.babel.js */ | |
import gulp from 'gulp'; | |
import webpack from 'webpack'; | |
import eslint from 'eslint/lib/cli'; | |
import run from 'run-sequence'; | |
import gutil from 'gulp-util'; | |
import { exec } from 'child_process'; | |
import del from 'del'; | |
import gulpConfig from './build/gulp.config'; | |
// Task name to compile production assets | |
const prodBuildTask = 'build'; | |
// Task name to start dev server | |
const startDevTask = 'start:dev'; | |
// Task name to start local server with production assets | |
const startProdTask = 'start:prod'; | |
// Defining environment | |
const isDevBuild = process.argv.indexOf(startDevTask) !== -1; | |
// Defining default task | |
const startTask = isDevBuild ? startDevTask : prodBuildTask; | |
// Importing gulp config (it's function, returns an object) | |
const config = gulpConfig(isDevBuild); | |
/* Run tasks */ | |
// Using `run-sequence` plugin to group [async tasks] in [sync groups of async tasks] | |
// 1 group: Cleaning public folder and linting scripts | |
// 2 group: Compiling assets and coping static stuff (fonts, favicon etc.) to public folder | |
// 3 group: Starting local Express servers | |
gulp.task('default', [startTask]); | |
gulp.task(prodBuildTask, done => { | |
run(['clean', 'lint'], ['bundle', 'copy'], done); | |
}); | |
gulp.task(startDevTask, done => { | |
run(['clean', 'lint'], ['bundle', 'copy'], ['server'], done); | |
}); | |
gulp.task(startProdTask, done => { | |
run(['clean', 'lint'], ['bundle', 'copy'], ['server'], done); | |
}); | |
/* Node servers starter */ | |
// Helper to start Express servers from gulp | |
const startServer = (serverPath, done) => { | |
// Defining production environment variable | |
const prodFlag = !isDevBuild ? 'NODE_ENV=production' : ''; | |
// Starting the server | |
const server = exec(`NODE_PATH=. ${prodFlag} node ${serverPath}`); | |
// Handling messages from server | |
server.stdout.on('data', data => { | |
// Checking if it's a message from webpack dev server | |
// that initial compile is finished | |
if (done && data === 'Webpack: Done!') { | |
// Notifying gulp that assets are compiled, this task is done | |
done(); | |
} else { | |
// Just printing output from server to console | |
gutil.log(data.trim()); | |
} | |
}); | |
// If there is an error - printing output to console and doing the BEEP | |
server.stderr.on('data', data => { | |
gutil.log(gutil.colors.red(data.trim())); | |
gutil.beep(); | |
}); | |
}; | |
/* Build bundles */ | |
gulp.task('bundle', done => { | |
if (isDevBuild) { | |
// Starting webpack dev server | |
startServer('server.dev.js', done); | |
} else { | |
// Just compiling assets | |
webpack(config.webpack).run(done); | |
} | |
}); | |
/* Start express servers */ | |
gulp.task('server', done => { | |
const servers = config.server.paths; | |
let queue = servers.length; | |
servers.forEach(server => { | |
startServer(server); | |
if (--queue === 0) done(); | |
}); | |
}); | |
/* Copy files to `public` */ | |
gulp.task('copy', done => { | |
const files = config.copy.files; | |
let queue = files.length; | |
files.forEach(file => { | |
const from = config.copy.from + file[0]; | |
const to = config.copy.to + (file[1] || file[0]); | |
exec(`cp -R ${from} ${to}`, err => { | |
if (err) { | |
gutil.log(gutil.colors.red(err)); | |
gutil.beep(); | |
} | |
if (--queue === 0) done(); | |
}); | |
}); | |
}); | |
/* Lint scripts */ | |
gulp.task('lint', done => { | |
eslint.execute('--ext .js,.jsx .'); | |
done(); | |
}); | |
/* Clean up public before build */ | |
gulp.task('clean', done => { | |
del(['./public/**/*'], done); | |
}); |
/* build/gulp.config.js */ | |
import webpackDevConfig from './webpack.config.dev'; | |
import webpackProdConfig from './webpack.config.prod'; | |
const _app = './app'; | |
const _public = './public'; | |
const _assets = `${_app}/assets`; | |
export default (isDevBuild) => { | |
return { | |
webpack: isDevBuild ? webpackDevConfig : webpackProdConfig, | |
server: { | |
paths: ['./server.app.js'] | |
}, | |
copy: { | |
from : _assets, | |
files: [ | |
[ '/tinies/favicon.ico', '/' ], | |
[ '/tinies/robots.txt', '/' ] | |
], | |
to: _public | |
} | |
}; | |
} |
For each environment there will be its own setup.
Production
First let’s define requirements for production build:
- Asset name should contain identifier for long-term caching purposes. We should include hash of the asset file in URL, thus when we’ll update the app, visitor’s browser will download updated asset, rather than using old one from cache. More detailed explanation on subject — in this medium post.
- Our app and vendor’s modules should be split in separated chunks. Every time we update the app, whole client’s asset is changed. So visitor have to re-download it, incl. vendor’s modules, that hasn’t been changed. If we will split client’s bundle in two chunks — app and vendor’s stuff — users won’t need to re-download vendor’s modules, because it’s in different chunk and vendor’s chunk is cached by the browser.
- CSS asset should be extracted out of javascript bundle and placed in head tag above initial html. If we won’t do this, visitors will see un-styled content for a moment, because initial html from server is placed above the javascript assets. You will notice this in development mode.
- Assets files should be minified & gzipped. So we will be serving smaller-sized assets to client.
For webpack compiler we have file with 2 end points:
/* build/bundles/app.js */ | |
import scripts from '../../app/bundles/app/initters/client.jsx'; | |
import styles from '../../app/bundles/app/layouts/Layout.styl'; |
Here is webpack production config:
/* build/webpack.config.prod.js */ | |
import webpack from 'webpack'; | |
import Extract from 'extract-text-webpack-plugin'; | |
import Gzip from 'compression-webpack-plugin'; | |
import Manifest from 'webpack-manifest-plugin'; | |
import ChunkManifest from 'chunk-manifest-webpack-plugin'; | |
import path from 'path'; | |
export default { | |
// Defining entry point | |
entry: { | |
// Bundle's entry points | |
app: './build/bundles/app.js', | |
// List of vendor's modules | |
// To extract them to separate chunk | |
vendor: ['react', 'react-router'] | |
}, | |
// Defining output params | |
output: { | |
path : './public/assets', | |
filename : '[name]-[chunkhash].js', | |
chunkFilename: '[name]-[chunkhash].js' | |
}, | |
// Defining resolver params | |
// On the server we use NODE_PATH=. | |
// On the client we use resolve.alias | |
resolve: { | |
alias: { | |
'app' : path.join(process.cwd(), 'app'), | |
'config': path.join(process.cwd(), 'config'), | |
'public': path.join(process.cwd(), 'public') | |
}, | |
extensions: ['', '.js', '.jsx'] | |
}, | |
devtool : false, | |
debug : false, | |
progress: true, | |
node : { | |
fs: 'empty' | |
}, | |
plugins: [ | |
// Extracting css | |
new Extract('[name]-[chunkhash].css'), | |
// Extracting vendor libs to separate chunk | |
new webpack.optimize.CommonsChunkPlugin({ | |
name : 'vendor', | |
chunks : ['app'], | |
filename : 'vendor-[chunkhash].js', | |
minChunks: Infinity | |
}), | |
// Defining some globals | |
new webpack.DefinePlugin({ | |
__CLIENT__ : true, | |
__SERVER__ : false, | |
__DEV__ : false, | |
__DEVTOOLS__ : false, | |
'process.env': { | |
'NODE_ENV': JSON.stringify('production') | |
} | |
}), | |
// Avoiding modules duplication | |
new webpack.optimize.DedupePlugin(), | |
// Extracting chunks filenames to json file | |
// Required to resolve assets filenames with hashes on server | |
// See `getAsset` helper in `app/libs/getAsset.js` | |
new Manifest(), | |
// Extracting chunks internal ids to json file | |
// Required to keep vendor's chunkhash unchanged | |
new ChunkManifest({ | |
filename : 'chunk-manifest.json', | |
manifestVariable: '__CHUNKS__' | |
}), | |
// Also required to keep vendor's chunkhash unchanged | |
new webpack.optimize.OccurenceOrderPlugin(), | |
// Minifying js assets | |
new webpack.optimize.UglifyJsPlugin({ | |
compress: { | |
'warnings' : false, | |
'drop_debugger': true, | |
'drop_console' : true, | |
'pure_funcs' : ['console.log'] | |
} | |
}), | |
// Gzipping assets | |
new Gzip({ | |
asset : '{file}.gz', | |
algorithm: 'gzip', | |
regExp : /\.js$|\.css$/ | |
}) | |
], | |
// Defining loaders | |
module: { | |
noParse: /\.min\.js$/, | |
loaders: [ | |
{ test : /\.jsx?$/, loader: 'babel?stage=0', exclude: /node_modules/ }, | |
{ | |
test : /\.styl$/, | |
loader: Extract.extract('style', 'css!autoprefixer?{browsers:["last 2 version"], cascade:false}!stylus') | |
}, | |
{ | |
test : /\.css$/, | |
loader: Extract.extract('style', 'css!autoprefixer?{browsers:["last 2 version"], cascade:false}') | |
} | |
] | |
} | |
} |
npm run prod and we have compiled assets in _public/assets_
folder ready to deploy.
Development
Requirements for development build:
- Local dev server should be hot reloadable. Webpage should automatically reflect all changes in js / css files on file save.
We don’t care about long-term caching, css extraction and assets minification in dev environment.
Hot reloadable dev server
The main idea of hot reloadable dev server, is that webpack is starting his own express server on some port (devPort in our config) and serving all assets from there with polling script on top:
<script src="[http://lvh.me:3001/assets/app.js](http://lvh.me:3001/assets/app.js)"></script>
Under the hood it observes the assets files, if it’s changed, it recompiles assets and sends changes to client. So you don’t need to reload page on every change. Here is how to set this up for React application:
/* build/webpack.config.dev.js */ | |
import webpack from 'webpack'; | |
import path from 'path'; | |
import appConfig from '../config/server.app'; | |
export default { | |
// Defining entry points for webpack dev server | |
entry: { | |
app: [ | |
`webpack-dev-server/client?http://lvh.me:${appConfig.devPort}`, | |
'webpack/hot/only-dev-server', | |
'./build/bundles/app.js' | |
], | |
vendor: ['react', 'react-router'] | |
}, | |
// Defining output for webpack dev server | |
output: { | |
path : path.join(process.cwd(), 'public', 'assets'), | |
filename : '[name].js', | |
publicPath: `http://lvh.me:${appConfig.devPort}/assets` | |
}, | |
resolve: { | |
alias: { | |
'app' : path.join(process.cwd(), 'app'), | |
'config': path.join(process.cwd(), 'config'), | |
'public': path.join(process.cwd(), 'public') | |
}, | |
extensions: ['', '.js', '.jsx'] | |
}, | |
// Source maps are slow, eval is fast | |
devtool : '#eval', | |
debug : true, | |
progress: true, | |
node : { | |
fs: 'empty' | |
}, | |
plugins: [ | |
// Enabling dev server | |
new webpack.HotModuleReplacementPlugin(), | |
// Don't update if there was error | |
new webpack.NoErrorsPlugin(), | |
new webpack.optimize.CommonsChunkPlugin({ | |
name : 'vendor', | |
chunks : ['app'], | |
filename : 'vendor.js', | |
minChunks: Infinity | |
}), | |
new webpack.DefinePlugin({ | |
__CLIENT__ : true, | |
__SERVER__ : false, | |
__DEV__ : true, | |
__DEVTOOLS__ : true, | |
'process.env': { | |
'NODE_ENV': JSON.stringify('development') | |
} | |
}) | |
], | |
module: { | |
noParse: /\.min\.js$/, | |
loaders: [ | |
// Notice `react-hot` loader, required for React components hot reloading | |
{ test : /\.jsx?$/, loaders: ['react-hot', 'babel?stage=0'], exclude: /node_modules/ }, | |
{ | |
test : /\.styl$/, | |
loader: 'style!css!autoprefixer?{browsers:["last 2 version"], cascade:false}!stylus' | |
}, | |
{ | |
test : /\.css$/, | |
loader: 'style!css!autoprefixer?{browsers:["last 2 version"], cascade:false}' | |
} | |
] | |
} | |
} |
And here is webpack development server, which we start by npm start (before express server of our app):
/* server.dev.js */ | |
require('babel/register')({ | |
extensions: ['.js', '.jsx'], | |
stage : 0 | |
}); | |
var webpack = require('webpack'); | |
var WebpackDevServer = require('webpack-dev-server'); | |
var webpackConfig = require('./build/webpack.config.dev'); | |
var serverConfig = require('./config/server'); | |
var initialCompile = true; | |
var compiler = webpack(webpackConfig); | |
var devServer = new WebpackDevServer(compiler, { | |
contentBase : 'http://lvh.me:' + serverConfig.devPort, | |
publicPath : webpackConfig.output.publicPath, | |
hot : true, | |
inline : true, | |
historyApiFallback: true, | |
quiet : false, | |
noInfo : false, | |
lazy : false, | |
stats : { | |
colors : true, | |
hash : false, | |
version : false, | |
chunks : false, | |
children: false | |
} | |
}); | |
devServer.listen(serverConfig.devPort, 'localhost', function(err) { | |
if (err) console.error(err); | |
console.log('=> 🔥 Webpack development server is running on port %s', serverConfig.devPort); | |
}); | |
// This part will notify gulp that assets are compiled | |
// Gulp will start application Express server then | |
// (and it'll pick up assets filenames from json manifest to inject them in html) | |
compiler.plugin('done', function() { | |
if (initialCompile) { | |
initialCompile = false; | |
process.stdout.write('Webpack: Done!'); | |
} | |
}); |
Now we can run npm start and open http://lvh.me in browser. Every time we make a changes in js or css files, page will be automatically updated by webpack dev server.
Conclusion
We’ve learned how to build simple universal javascript app using React (Dude! It’s not simple at all!). But this is only beginning. Real-world apps is much more complex, so here is the questions we have to answer next:
- How to manage state in our app?
- How to prefetch data on the server, so we can render html with data from API and serve it to client?
- How to secure it?
- How to deploy it to production server?
Stay tuned to get the answers!
Part I: Planning the application
Part III: Building Universal app