Towards Universal JavaScript
Agenda
- What ? Why Universal JavaScript ?
- How to build a Universal JavaScript app ?
Sarmad Sangi
Works at RedMart
redmart.com
What is Universal JavaScript ?
Universal
JavaScript code which is environment agnostic.
https://medium.com/@mjackson/universal-javascript-4761051b7ae9
Universal
JavaScript code which is environment agnostic.
Isomorphic
That any given line of code (with notable exceptions) can execute both on the client and the server
https://medium.com/@mjackson/universal-javascript-4761051b7ae9
Why Universal JavaScript ?
SEO
Performance
Maintainability
SEO
<!doctype html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Some Single Page Application</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/main.css">
</head>
<body>
<div class="root"></div>
<script src="/app.js"></script>
</body>
</html>
How bots see our javascript applications
SEO
# Pre-build the site for robots
if ($http_user_agent ~* "(alltheweb|baidu|bingbot|google|googlebot|msnbot|slurp|facebookexternalhit)") {
rewrite ^ $request_uri break;
expires 24h;
add_header Pragma public;
add_header Cache-Control 'max-age=86400, must-revalidate';
# Reverse proxy to server running PhantomJs
proxy_pass http://$render_services_host:8008;
}
Hacks that we have to do to please these bots
SEO
With Universal JavaScript approach we can just render the same application on server
<html>
<head>
<title>Universal JavaScript</title>
<link rel="stylesheet" media="screen, projection" href="static/style.css">
</head>
<body>
<div id="root">
<div data-reactid=".1hxkpv4740" data-react-checksum="2883365">
<form data-reactid=".1hxkpv4740.0"><input type="text" placeholder="Search for products and brands" data-reactid=".1hxkpv4740.0.0"></form><div data-reactid=".1hxkpv4740.1"></div><div data-reactid=".1hxkpv4740.2"><a class="card" target="_blank" href="https://redmart.com/product/strongbow-apple-cider-gold-16652" data-reactid=".1hxkpv4740.2.0"><p data-reactid=".1hxkpv4740.2.0.0">Strongbow Apple Cider Gold</p><img src="https://s3-ap-southeast-1.amazonaws.com/media.redmart.com/newmedia/150x/i/m/5035766044772_3_1426249296514.jpg" data-reactid=".1hxkpv4740.2.0.1"></a><a class="card" target="_blank" href="https://redmart.com/product/bananas-13925" data-reactid=".1hxkpv4740.2.1"><p data-reactid=".1hxkpv4740.2.1.0">Small Bananas</p><img src="https://s3-ap-southeast-1.amazonaws.com/media.redmart.com/newmedia/150x/i/m/4935850351087_3_1432532094722.jpg" data-reactid=".1hxkpv4740.2.1.1"></a><a class="card" target="_blank" href="https://redmart.com/product/gardenia-finegrain-wholemeal-bread" data-reactid=".1hxkpv4740.2.2"><p data-reactid=".1hxkpv4740.2.2.0">Gardenia Finegrain Wholemeal Bread</p><img src="https://s3-ap-southeast-1.amazonaws.com/media.redmart.com/newmedia/150x/i/m/RM_IMG_11671F.JPG" data-reactid=".1hxkpv4740.2.2.1"></a><a class="card" target="_blank" href="https://redmart.com/product/nestle-natural-set-yogurt-13989" data-reactid=".1hxkpv4740.2.3"><p data-reactid=".1hxkpv4740.2.3.0">Nestle Natural Set Yogurt</p><img src="https://s3-ap-southeast-1.amazonaws.com/media.redmart.com/newmedia/150x/i/m/IMG0051_1411017267823.JPG" data-reactid=".1hxkpv4740.2.3.1"></a><a class="card" target="_blank" href="https://redmart.com/product/perrier-natural-mineral-water" data-reactid=".1hxkpv4740.2.4"><p data-reactid=".1hxkpv4740.2.4.0">Perrier Natural Mineral Water</p><img src="https://s3-ap-southeast-1.amazonaws.com/media.redmart.com/newmedia/150x/i/m/img_7474.jpg" data-reactid=".1hxkpv4740.2.4.1"></a><a class="card" target="_blank" href="https://redmart.com/product/dasani-drinking-water-l-case" data-reactid=".1hxkpv4740.2.5"><p data-reactid=".1hxkpv4740.2.5.0">Dasani Drinking Water - Case</p><img src="https://s3-ap-southeast-1.amazonaws.com/media.redmart.com/newmedia/150x/i/m/Dasani-Drinking-Water-12x15L1401883801960_1420546020455.jpg" data-reactid=".1hxkpv4740.2.5.1"></a></div>
</div>
</div>
<script src="static/bundle.js"></script>
</body>
</html>
Performance
Better initial load time
Download HTML (document)
Download JavaScript (app.js)
Makes Ajax calls
Download HTML (document)
Download JavaScript (app.js)
Do other stuff
Client Side Rendering
Server Side Rendering
User sees content
User sees content
Maintainability
Shared logic between server and clients
How to build a Universal JavaScript app ?
Lets build one
Lets build one
App.js
Input.js
Progress.js
SearchResult.js
Component hierarchy
Lets build one
App.js
Input.js
Progress.js
SearchResult.js
App.js
import React, { Component } from 'react'
import Input from '../components/Input'
import SearchResult from '../components/SearchResult'
import ResultCard from '../components/ResultCard'
import products from '../data/products'
export default class App extends Component {
// set initial state
constructor (props) {}
// render state
render () {}
// do required logic on search input submit
handleOnSubmit (e) {}
// get products based on input (in this case we will just get some randoms stuff)
getProducts () {}
}
Lets build one
App.js
render () {
const { progress, products } = this.state
const productResultSet = products.map((product) => {
return <ResultCard {...product} />
})
return (
<div>
<Input
placeholder='Search for products and brands'
handleOnSubmit={::this.handleOnSubmit} />
<div>{progress}</div>
<SearchResult>
{productResultSet}
</SearchResult>
</div>
)
}
Lets build one
client.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './containers/App'
ReactDOM.render(
<App />,
document.getElementById('root')
)
Lets build one
index.html
<!doctype html>
<html>
<head>
<title>Universal JavaScript</title>
<link rel="stylesheet" media="screen, projection" href='static/style.css'>
</head>
<body>
<div id='root'></div>
<script src='static/bundle.js'></script>
</body>
</html>
Lets build one
server.js
import express from 'express'
import React, { Component } from 'react'
import { renderToString } from 'react-dom/server'
import App from './containers/App'
import htmlWrapper from './Html'
const server = express()
server.use('/static', express.static(__dirname + '/../static'));
server.use((req, res) => {
res.send(htmlWrapper(renderToString(<App />)))
})
server.listen(3000)
Done
Not really!
Tricky Parts
Lets look at Real App
Tricky Parts
Maintaining State between server and client
// Store created on Server
let store = createStore(AppReducers)
// Dehydrating for client
const getCurrentState = store.getState()
// Somewhere in our html
<script>
window.STATE_FROM_SERVER = ${JSON.stringify(getCurrentState)}
</script>
// Rehydrating on client
let store = createStore(AppReducers, window.STATE_FROM_SERVER)
Tricky Parts
Modules/Libraries that work both on Server and Client
// DON'T DO THIS
if (__SERVER__) {
http.request(options, processData)
} else {
$.ajax(options).done(processData)
}
// TRY universal/isomorphic modules
import fetch from 'isomorphic-fetch'
fetch(url).then(processData)
Tricky Parts
Effectively handling data fetching
// For super simple apps, fetch and
// setState just before generating html
fetchSearchTermResult(query, function (json) {
// create new state after data fetch
const newState = {
...getStore().getState(),
loading: false,
loaded: true,
query: query,
payload: json
}
// send down the response with new state
res.send(
generateInitialHtml(
getStore(newState),
getStore(newState).getState(),
renderProps
)
)
})
Pre Fetch
Fetch all the page data on server before sending down the html
Tricky Parts
Effectively handling data fetching
Pre Fetch
Fetch all the page data on server before sending down the html
// Handler Component
export default class Landing extends Component {
static fetchData (store) {
return store.dispatch(fetchBothCategoriesAndProductList())
}
}
// dataFetchMiddleware loops through all the components,
// looks for all fetchData and resolve those promises
const createStoreWithMiddleware = applyMiddleware(
dataFetchMiddleware
)(createStore)
let store = createStoreWithMiddleware(reducers, initialState)
Tricky Parts
Effectively handling data fetching
Deferred Fetch
Pre Fetch
// Handler Component
export default class Landing extends Component {
static fetchData (store) {
return store.dispatch(fetchCategories())
}
componentDidMount () {
store.dispatch(fetchProductList())
}
}
// dataFetchMiddleware loops through all the components,
// looks for all fetchData and resolve those promises
const createStoreWithMiddleware = applyMiddleware(
dataFetchMiddleware
)(createStore)
let store = createStoreWithMiddleware(reducers, initialState)
Tricky Parts
Effectively handling data fetching
Deferred Fetch
Pre Fetch
// Handler Component with react-fetcher + some promise middleware for redux
@prefetch(({ dispatch }) => dispatch(fetchCategories())
@defer(({ dispatch }) => dispatch(fetchProductList())
export default class Landing extends Component {
// Just render data
}
Code
Awesome Resources
Towards Universal JavaScript
By Sarmad Sangi
Towards Universal JavaScript
Youtube video https://www.youtube.com/watch?v=ld3X8PTOgQQ Github repo https://github.com/Redmart/universal
- 3,761