Towards Universal JavaScript

Agenda

  1. What ? Why Universal JavaScript ?
  2. 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

Made with Slides.com