Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building Universal Web Apps With React

Building Universal Web Apps With React

ForwardJS 2017 - An overview of isomorphic architecture, including why you would want to build your app this way and what some of the challenges are.

Elyse Kolker Gordon

March 01, 2017
Tweet

More Decks by Elyse Kolker Gordon

Other Decks in Technology

Transcript

  1. Building Universal Web Apps with React Elyse Kolker Gordon Twitter:

    @sfdrummerjs, Github: @elyseko Web Lead, Vevo Code: bit.ly/universal-app-react
  2. Overview of Universal/Isomorphic Web Apps Why would you want to

    build an universal web app Universal App Architecture Challenges and Tradeoffs Topics:
  3. #1 SEO Getting your website (or cat video) to rank

    the highest can make or break your success as a business. Why build it?
  4. #2 Perceived Performance Users should think the site is fast.

    This means showing content (not loading spinners) as soon as possible. Why build it?
  5. Everything is JavaScript! Most of your code will run on

    both the server and the browser. #3 Maintenance and Developer benefits Why build it?
  6. In this section Technologies required to build an universal app

    Loading a single page of the app in the isomorphic flow Architecture Code: bit.ly/universal-app-react
  7. In this section Technologies required to build an universal app

    Loading a single page of the app in the isomorphic flow Architecture Code: bit.ly/universal-app-react
  8. In this section Technologies required to build an universal app

    Loading a single page of the app in the isomorphic flow Architecture Code: bit.ly/universal-app-react
  9. Why webpack? NPM packages ES6/ES7 with babel CSS in your

    browser bundle Advanced features Architecture: webpack
  10. module.exports = {
 entry: "./src/main.jsx",
 devtool: "source-map",
 output: {
 path:

    __dirname + '/ src/',
 filename: "browser.js"
 }, Architecture: webpack webpack.config.js
  11. module.exports = {
 ... Architecture: webpack module: {
 loaders: [


    {
 test: /\.(jsx|es6)$/,
 exclude: /node_modules|examples/,
 loader: "babel-loader"
 },
 {
 test: /\.css$/,
 loaders: ['style', 'css']
 }
 ] webpack.config.js
  12. import React from 'react';
 import { match, RouterContext } from

    'react-router';
 import { routes } from '../shared/sharedRoutes';
 
 export default function renderView(req, res, next) {
 }
 Architecture: React Router renderView.jsx
  13. import React from 'react';
 import { match, RouterContext } from

    'react-router';
 import { routes } from '../shared/sharedRoutes';
 
 export default function renderView(req, res, next) {
 }
 Architecture: React Router match(matchOpts, handleMatchResult); renderView.jsx
  14. import React from 'react';
 import { match, RouterContext } from

    'react-router';
 import { routes } from '../shared/sharedRoutes';
 
 export default function renderView(req, res, next) {
 const matchOpts = {
 routes: routes(),
 location: req.url
 };
 
 
 }
 Architecture: React Router match(matchOpts, handleMatchResult); renderView.jsx match(matchOpts, handleMatchResult);
  15. import React from 'react';
 import { match, RouterContext } from

    'react-router';
 import { routes } from '../shared/sharedRoutes';
 
 export default function renderView(req, res, next) {
 const matchOpts = {
 routes: routes(),
 location: req.url
 };
 
 
 }
 Architecture: React Router const handleMatchResult = (error, redirectLocation, renderProps) => {
 if (!error && !redirectLocation && renderProps) { // render code } } match(matchOpts, handleMatchResult); renderView.jsx match(matchOpts, handleMatchResult); const matchOpts = {
 routes: routes(),
 location: req.url
 };

  16. <Route path="/" component={App}>
 <Route path="/cart" component={Cart} />
 <Route path="/cart/payment" component={Payment}

    />
 <Route path="/products" component={Products} />
 <Route path="/profile" component={Profile} />
 <Route path="/login" component={Login} />
 </Route> Architecture: Create the Cart sharedRoutes.jsx
  17. Architecture: Create the Cart export const routes = () =>

    { } <Route path="/" component={App}>
 <Route path="/cart" component={Cart} />
 <Route path="/cart/payment" component={Payment} />
 <Route path="/products" component={Products} />
 <Route path="/profile" component={Profile} />
 <Route path="/login" component={Login} />
 </Route> sharedRoutes.jsx
  18. return (
 <div className="item">
 <div className="ui tiny image">
 <img src={props.thumbnail}

    alt="cart" />
 </div>
 <div className="middle aligned content">
 {props.name}
 </div>
 <div className="right aligned content">
 ${props.price}
 </div>
 </div>
 );
 const Item = (props) => {
 };
 Architecture: Create the Cart item.jsx
  19. return (
 <div className="item">
 <div className="ui tiny image">
 <img src={props.thumbnail}

    alt="cart" />
 </div>
 <div className="middle aligned content">
 {props.name}
 </div>
 <div className="right aligned content">
 ${props.price}
 </div>
 </div>
 );
 const Item = (props) => {
 };
 Architecture: Create the Cart item.jsx
  20. return (
 <div className="item">
 <div className="ui tiny image">
 <img src={props.thumbnail}

    alt="cart" />
 </div>
 <div className="middle aligned content">
 {props.name}
 </div>
 <div className="right aligned content">
 ${props.price}
 </div>
 </div>
 );
 return (
 <div className="item">
 <div className="ui tiny image">
 <img src={props.thumbnail} alt="cart" />
 </div>
 <div className="middle aligned content">
 {props.name}
 </div>
 <div className="right aligned content">
 ${props.price}
 </div>
 </div>
 );
 const Item = (props) => {
 };
 const Item = (props) => {
 };
 Architecture: Create the Cart item.jsx
  21. render() {
 return (
 <div className="cart main ui segment">
 <div

    className="ui segment divided items">
 {this.renderItems()}
 </div> </div>
 );
 } Architecture: Create the Cart cart.jsx
  22. render() {
 return (
 <div className="cart main ui segment">
 <div

    className="ui segment divided items">
 {this.renderItems()}
 </div> </div>
 );
 } Architecture: Create the Cart <div className="ui right rail">
 <div className="ui segment">
 <span>Total: </span><span>${this.getTotal()}</span>
 <button onClick={this.proceedToCheckout} className="ui positive basic button">
 Checkout
 </button>
 </div> </div> render() {
 return (
 <div className="cart main ui segment">
 <div className="ui segment divided items">
 {this.renderItems()}
 </div> </div>
 );
 } cart.jsx
  23. 
 import cartActions from '../shared/cart-action-creators.es6';
 
 export class CartComponent extends

    Component {
 
 static loadData() {
 return [
 cartActions.getCartItems
 ];
 }
 render() {}
 }
 Architecture: Create the Cart cart.jsx
  24. 
 
 export class CartComponent extends Component {} 
 function

    mapStateToProps(state) {}
 
 function mapDispatchToProps(dispatch) {}
 
 export default connect(mapStateToProps, mapDispatchToProps) (CartComponent); Architecture: Create the Cart cart.jsx
  25. import fetch from 'isomorphic-fetch';
 
 export const GET_CART_ITEMS = 'GET_CART_ITEMS';


    
 export function getCartItems() { Architecture: Create the Cart cart-action-creators.es6
  26. import fetch from 'isomorphic-fetch';
 
 export const GET_CART_ITEMS = 'GET_CART_ITEMS';


    
 export function getCartItems() {
 return (dispatch) => {
 return fetch('http://localhost:3000/api/user/cart', {
 method: 'GET'
 }) Architecture: Create the Cart cart-action-creators.es6
  27. import fetch from 'isomorphic-fetch';
 
 export const GET_CART_ITEMS = 'GET_CART_ITEMS';


    
 export function getCartItems() {
 return (dispatch) => {
 return fetch('http://localhost:3000/api/user/cart', {
 method: 'GET'
 }) Architecture: Create the Cart cart-action-creators.es6 .then((response) => {
 return response.json().then((data) => {
 return dispatch({
 type: GET_CART_ITEMS,
 data: data.items
 });
 });
 })
  28. Architecture: Create the Cart import { GET_CART_ITEMS } from './cart-action-creators.es6';


    
 export default function cart(state = {}, action) {
 switch (action.type) {
 case GET_CART_ITEMS:
 return {
 ...state,
 items: action.data
 };
 }
 } cart-reducer.es6
  29. Architecture: Create the Cart import { GET_CART_ITEMS } from './cart-action-creators.es6';


    
 export default function cart(state = {}, action) {
 switch (action.type) {
 case GET_CART_ITEMS:
 return {
 ...state,
 items: action.data
 };
 }
 } import { GET_CART_ITEMS } from './cart-action-creators.es6';
 
 export default function cart(state = {}, action) {
 switch (action.type) {
 case GET_CART_ITEMS:
 return {
 ...state,
 items: action.data
 };
 }
 } cart-reducer.es6
  30. Architecture: Server setup export default function renderView(req, res, next) {


    const matchOpts = {};
 const handleMatchResult = (error, redirectLocation, renderProps) => {
 if (!error && !redirectLocation && renderProps) {
 const store = initRedux();
 let actions = renderProps.components.map((component) => {
 // return actions from loadData() on each component
 }); renderView.jsx
  31. Architecture: Server setup 
 Promise.all(promises).then(() => {
 const serverState =

    store.getState();
 
 const stringifiedServerState = JSON.stringify(serverState); 
 Promise.all(promises).then(() => {
 const serverState = store.getState();
 
 renderView.jsx
  32. Architecture: Server setup 
 Promise.all(promises).then(() => {
 const serverState =

    store.getState();
 
 const stringifiedServerState = JSON.stringify(serverState); 
 Promise.all(promises).then(() => {
 const serverState = store.getState();
 
 
 const app = renderToString(
 <Provider store={store}>
 <RouterContext routes={routes} {...renderProps} />
 </Provider>
 ); const stringifiedServerState = JSON.stringify(serverState); renderView.jsx
  33. Architecture: Server setup 
 Promise.all(promises).then(() => {
 const serverState =

    store.getState();
 
 const stringifiedServerState = JSON.stringify(serverState); 
 Promise.all(promises).then(() => {
 const serverState = store.getState();
 
 
 const app = renderToString(
 <Provider store={store}>
 <RouterContext routes={routes} {...renderProps} />
 </Provider>
 ); const stringifiedServerState = JSON.stringify(serverState); 
 const html = renderToString(
 <HTML html={app} serverState={stringifiedServerState} />
 );
 
 
 const app = renderToString(
 <Provider store={store}>
 <RouterContext routes={routes} {...renderProps} />
 </Provider>
 ); renderView.jsx
  34. Architecture: Server setup 
 Promise.all(promises).then(() => {
 const serverState =

    store.getState();
 
 const stringifiedServerState = JSON.stringify(serverState); 
 Promise.all(promises).then(() => {
 const serverState = store.getState();
 
 
 const app = renderToString(
 <Provider store={store}>
 <RouterContext routes={routes} {...renderProps} />
 </Provider>
 ); const stringifiedServerState = JSON.stringify(serverState); 
 const html = renderToString(
 <HTML html={app} serverState={stringifiedServerState} />
 );
 
 return res.send(`<!DOCTYPE html>${html}`); 
 const app = renderToString(
 <Provider store={store}>
 <RouterContext routes={routes} {...renderProps} />
 </Provider>
 ); 
 const html = renderToString(
 <HTML html={app} serverState={stringifiedServerState} />
 );
 
 renderView.jsx
  35. Architecture: Server setup const HTML = (props) => {
 return

    (
 <html lang="en">
 <head></head>
 <body>
 <div
 id="react-content"
 dangerouslySetInnerHTML={{ __html: props.html }}/>
 <script dangerouslySetInnerHTML={{
 __html: `
 window.__SERIALIZED_STATE__ =
 JSON.stringify(${props.serverState})
 `
 }}/>
 <script type="application/javascript" src="/browser.js" />
 </body>
 </html>
 );
 }; html.jsx
  36. Architecture: Server setup const HTML = (props) => {
 return

    (
 <html lang="en">
 <head></head>
 <body>
 <div
 id="react-content"
 dangerouslySetInnerHTML={{ __html: props.html }}/>
 <script dangerouslySetInnerHTML={{
 __html: `
 window.__SERIALIZED_STATE__ =
 JSON.stringify(${props.serverState})
 `
 }}/>
 <script type="application/javascript" src="/browser.js" />
 </body>
 </html>
 );
 }; const HTML = (props) => {
 return (
 <html lang="en">
 <head></head>
 <body>
 <div
 id="react-content"
 dangerouslySetInnerHTML={{ __html: props.html }}/>
 <script dangerouslySetInnerHTML={{
 __html: `
 window.__SERIALIZED_STATE__ =
 JSON.stringify(${props.serverState})
 `
 }}/>
 <script type="application/javascript" src="/browser.js" />
 </body>
 </html>
 );
 }; html.jsx
  37. Architecture: Server setup const HTML = (props) => {
 return

    (
 <html lang="en">
 <head></head>
 <body>
 <div
 id="react-content"
 dangerouslySetInnerHTML={{ __html: props.html }}/>
 <script dangerouslySetInnerHTML={{
 __html: `
 window.__SERIALIZED_STATE__ =
 JSON.stringify(${props.serverState})
 `
 }}/>
 <script type="application/javascript" src="/browser.js" />
 </body>
 </html>
 );
 }; const HTML = (props) => {
 return (
 <html lang="en">
 <head></head>
 <body>
 <div
 id="react-content"
 dangerouslySetInnerHTML={{ __html: props.html }}/>
 <script dangerouslySetInnerHTML={{
 __html: `
 window.__SERIALIZED_STATE__ =
 JSON.stringify(${props.serverState})
 `
 }}/>
 <script type="application/javascript" src="/browser.js" />
 </body>
 </html>
 );
 }; const HTML = (props) => {
 return (
 <html lang="en">
 <head></head>
 <body>
 <div
 id="react-content"
 dangerouslySetInnerHTML={{ __html: props.html }}/>
 <script dangerouslySetInnerHTML={{
 __html: `
 window.__SERIALIZED_STATE__ =
 JSON.stringify(${props.serverState})
 `
 }}/>
 <script type="application/javascript" src="/browser.js" />
 </body>
 </html>
 );
 }; html.jsx
  38. Architecture: Server setup const HTML = (props) => {
 return

    (
 <html lang="en">
 <head></head>
 <body>
 <div
 id="react-content"
 dangerouslySetInnerHTML={{ __html: props.html }}/>
 <script dangerouslySetInnerHTML={{
 __html: `
 window.__SERIALIZED_STATE__ =
 JSON.stringify(${props.serverState})
 `
 }}/>
 <script type="application/javascript" src="/browser.js" />
 </body>
 </html>
 );
 }; const HTML = (props) => {
 return (
 <html lang="en">
 <head></head>
 <body>
 <div
 id="react-content"
 dangerouslySetInnerHTML={{ __html: props.html }}/>
 <script dangerouslySetInnerHTML={{
 __html: `
 window.__SERIALIZED_STATE__ =
 JSON.stringify(${props.serverState})
 `
 }}/>
 <script type="application/javascript" src="/browser.js" />
 </body>
 </html>
 );
 }; const HTML = (props) => {
 return (
 <html lang="en">
 <head></head>
 <body>
 <div
 id="react-content"
 dangerouslySetInnerHTML={{ __html: props.html }}/>
 <script dangerouslySetInnerHTML={{
 __html: `
 window.__SERIALIZED_STATE__ =
 JSON.stringify(${props.serverState})
 `
 }}/>
 <script type="application/javascript" src="/browser.js" />
 </body>
 </html>
 );
 }; const HTML = (props) => {
 return (
 <html lang="en">
 <head></head>
 <body>
 <div
 id="react-content"
 dangerouslySetInnerHTML={{ __html: props.html }}/>
 <script dangerouslySetInnerHTML={{
 __html: `
 window.__SERIALIZED_STATE__ =
 JSON.stringify(${props.serverState})
 `
 }}/>
 <script type="application/javascript" src="/browser.js" />
 </body>
 </html>
 );
 }; html.jsx
  39. 
 const initialState = JSON.parse(window.__SERIALIZED_STATE__);
 const store = initRedux(initialState);
 Architecture:

    Browser setup 
 function init() {
 ReactDOM.render(
 <Provider store={store}>
 <Router routes={sharedRoutes(store)} history={browserHistory} />
 </Provider>, document.getElementById('react-content'));
 }
 
 init(); 
 const initialState = JSON.parse(window.__SERIALIZED_STATE__);
 const store = initRedux(initialState);

  40. Server Performance Strategies Only render above the fold content on

    the server (SEO tradeoff) Use streams Caching Challenges and Tradeoffs
  41. Two Options In memory caching CDN/Edge caching Walmart Labs Server

    Side Render Cache (Electrode) Challenges and Tradeoffs