isomorphic-react-workshop



isomorphic-react-workshop

0 1


isomorphic-react-workshop

Talk on how to build isomorphic web apps in React, complete with example code

On Github DonaldWhyte / isomorphic-react-workshop

Building Isomorphic Web Applications

Using React and Node

Created by Donald Whyte / @donald_whyte

Why Build Web Applications?

  • Wealth of open-source libraries and tools
  • Easy to run the whole stack locally
    • no reliance on heavy system dependencies that are difficult to isolate
    • no need to rely on shared internal infrastructure
  • Increases developer productivity
    • allows for rapid development of new features

Always good to try new things!

Goal of this Talk

  • Understand how modern web apps are structured
  • Get an introduction to some frameworks and libraries which can implement these apps
  • Walkthrough an example application
    • React application
    • backed by Node API

Scope

  • Building a web application using a modern architecture
  • Code examples given, but focus is on high-level process

Pre-requisites

  • Intermediate knowledge of JavaScript
  • Understanding of:
    • how web pages work (i.e. HTML/CSS/JavaScript)
    • service-orientated architectures
    • and a little bit about cloud computing

Architecture

Some history...

Mainframes

Thin client, heavy server

Desktop Clients

Heavy client, thin server

Birth of the Web

Thin client, heavy server

Single Page Application

Heavy client, thin server

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Twitter Searcher</title>
</head>
<body>
  <div id="app-view"></div>
  <script type="application/javascript" src="/assets/bundle.js">
  </script>
</body>
</html>

SAPs are becoming the standard approach in web dev.

Building SAPs will be the focus of this talk.

Some Problems

  • search engine optimisation
  • performance
    • responsiveness / page coherency
  • maintainability

Isomorphic Apps

  • also known as universal apps
  • render entire page in its initial state on server
    • valid, renderable HTML that represents the page
  • any state changes will be rendered by the SAP on the client browser

The Stack

Web App (Client & Server)

Many popular frameworks:

  • AngularJs
  • Meteor
  • Backbone
  • React
  • etc.

React will be used for this workshop

React

  • open-source JavaScript UI framework
  • developed by Facebook
  • separates a web page into components
    • combined into a hierarchy and rendered
  • become popular amongst many heavy-hit web apps
    • Instagram, AirBnB, Salesforce, Wolfram Alpha

Model-View Controller

React is not an MVC framework

  • the V in MVC
  • but it's not just a HTML template engine
    • HTML templates just generate textual markup
  • React uses the virtual DOM
    • its own lightweight representation of a doc
  • virtual DOM is build from React components
    • isolated, testable view components
    • which handle their own state

Why React?

  • React uses its virtual DOM for rendering
    • does not depend on browsers
    • allows page to be rendered on the client and server
    • making it easier to build isomorphic apps
  • React components are easily testable
  • done right, components will also be reusable
  • free to use whatever you want for the model/controller

Server

  • can use any language
  • but we're building an isomorphic app
  • requires ability to render HTML on client and server
  • duplicating render logic won't scale
  • unless we use JavaScript for the back-end
    • share React render code!

  • JavaScript runtime environment
  • primarily built for server-side web applications
  • event-driven, non-blocking IO
  • massive adoption in the open-source community
  • growing adoption in enterprise

ECMAScript 6 (ES6)

  • ES6 is the latest finalised JavaScript/ECMAScript standard
  • larger standard library
    • includes promises, new collections, typed arrays
  • new language constructs
    • e.g. classes, generators, arrow functions
  • can use both client and server side using Babel

Classes

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    toString() {
        return `(${this.x}, ${this.y})`;
    }
}

Inheritence

class ColorPoint extends Point {
    constructor(x, y, color) {
        super(x, y);
        this.color = color;
    }
    toString() {
        return super.toString() + ' in ' + this.color;
    }
}

Importing Other Modules

Modules are JavaScript files.

// import foo.js in same dir as current file
import foo from './foo';
foo.foobar(42);

// import specific variables/functions from a module
import { foobar } from './foo';
foobar(42);

Exporting Symbols

foo.js:

export function foobar(num) {
  console.log('FOOBAR:', num);
}

Exporting Entire Objects

person.js:

export default {
  name: 'Donald',
  age: 24
};

another_file.js

import person as './person';
console.log(person.name);

// outputs 'Donald'

Promises

es6fiddle

let readDb = new Promise(function(resolve, reject) {
  let row = readRowFromDb();
  if (row) {
    resolve(row);
  } else if {
    reject('Could not find');
  }
});

function onSuccess(row) {
  console.log('ROW:', row);
}

function onFailure(err) {
  console.error(err);
}

readDb.then(onSuccess, onFailure);

Summary

  • React for rendering DOM elements on UI
  • NodeJs for back-end server and APIs
  • ES6 for both front-end and back-end code

Let's Build Something!

Twitter Search Engine

What Do We Need?

  • React single page application
  • web server to render initial page and send it to browser
    • along with the React app code
  • twitterSearchApi -- RESTful API that will run Twitter searches

Architecture

Steps

Build single page application Serve it as an isomorphic app Implement twitterSearchApi

1. Single Page Application

The DOM

In a Nutshell

Initial Render
  • Represent page elements using virtual DOM
  • Defines an initial state for each page element
  • Use virtual DOM to render actual HTML

In a Nuthshell

Stage Changes
  • When state of a component changes:
    • re-render entire virtual DOM
    • very fast
  • Diff old and new virtual DOMs
    • use diff to compute minimum set of HTML DOM changes to render the new state
  • Apply changes to render new state in the browser

What's Wrong with Regular DOM?

DOM was never optimised for creating dynamic UIs

The Virtual DOM

  • React's local and simplified copy of the HTML DOM
  • Can render HTML without browser DOM
  • Allows for both client and server side rendering using the same code
  • Rendering the virtual DOM is much faster than rendering the HTML DOM

Building Blocks

  • ReactElement
  • ReactNode
  • ReactComponent

ReactElement — jsfiddle

Lowest type in the virtual DOM, similar to an XML element.

These can represent complex UI components.

let props = { id: 'root-container' };
let children = 'just text, but can be list of child elements';
let root = React.createElement('div', props, children);

// Render virtual DOM element into real DOM,
// inserting into existing element on page.
ReactDOM.render(root, document.getElementById('app-container'));

ReactNode

  • Building blocks for virtual DOM hierarchy
  • Can be:
    • ReactElement
    • string
    • number
    • array of ReactNodes

ReactComponent

A specification on how to build ReactElement.

ReactElements are essentially instantiations of components.

jsfiddle

import React from 'react';

class Message extends React.Component {
    render() {
        return <div className='message'>{this.props.contents}</div>;
    }
}
import ReactDOM from 'react';

ReactDOM.render(
    <Message contents="Hello world!" />,
    document.getElementById('app-view'),
    function() {
        console.log('Callback that executes after rendering');
    }
);

wat?

return <div className='message'>{this.props.contents}</div>;

JSX

  • Preprocessor step that adds XML syntax to JavaScript
  • JSX elements are parsed and replaced w/ generated code
    • often parsed in-browser for development
    • typically parsed prior to deploying for efficiency
  • Optional
Input (JSX)
<Message contents="Hello world!" />
Output (JavaScript)
React.createElement(Message, { contents: 'Hello world!' });

Nested Components

Input (JSX)
import config from './config';

let App = (
  <Form endpoint={config.submitEndpoint}>
    <FormRow>
      <FormLabel text="Name" />
      <FormInput />
    </FormRow>
    <FormRow>
      <FormLabel text="Age" />
      <FormInput />
    </FormRow>
  </Form>
);

Nested Components

Output (JavaScript)
import config from './config';

let app = React.createElement(
    Form, { endpoint: config.submitEndpoint },
    [
        React.createElement(
            FormRow, {},
            [
                React.createElement(FormLabel, { text: 'Name' }),
                React.createElement(FormInput, {}),
            ]
        ),
        React.createElement(
            FormRow, {},
            [
                React.createElement(FormLabel, { text: 'Age' }),
                React.createElement(FormInput, {}),
            ]
        ),
    ]
);
            

Props

  • every element and component has a props attribute
  • short for properties
  • plain JS object
  • represents the component's configuration or parameters
  • used to render the component in different ways
class TextEntry extends React.Component {
  render() {
    return <input type="text" value={this.props.initialValue} />;
  }
};

React.render(
  <TextEntry initialValue="42" />,
  document.getElementById('container'));

props are Not State

  • props are received from parent components
  • intended to be declarative
  • only set at element definition time
  • component cannot change its own props
    • but it is responsible for putting together the props of its child components

State

  • attributes of a component that can change
    • typically due to user interaction
  • define an initial state
  • state can be used to define props for child components
  • if any stateful attribute is mutated:
    • virtual DOM is rendered again
  • always change state using this.setState()

jsfiddle

class TextEntry extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0
    };
    this.onChange = this.onChange.bind(this);
  }

  onChange(event) {
    if (event.target.value === '') { event.target.value = 0; }
    let val = parseInt(event.target.value);
    if (!isNaN(val)) { this.setState({ val: val }); }
  };

  render() {
    return (
      <div>
        <input type="text" value={this.state.val} onChange={this.onChange} />
        <p>Weighted value: {this.state.val * 2}</p>
      </div>);
  }
}

Props vs. State

Props and state both:

  • plain JS objects
  • trigger a render update when modified
  • deterministic

Which to Use?

If a component needs to alter one of its attributes

that attribute should be part of its state

otherwise it should just be a prop for that component.

Back to the Twitter Searcher...

Twitter Search Components

Let's expand this model to an entire site

React Router

  • Client and server side routing library
  • Keeps your UI in sync with the URL
  • Takes a path and uses it to generate the correct components

router.js:

import React from 'react';
import { Router, Route, IndexRoute, browserHistory } from 'react-router';

// Top-level ReactElement that stores application.
// Apply common styles, headers, footers, etc. here
function App(props) {
  return (
    <div id='app-container'>
      <h1>Twitter Searcher</h1> { this.props.children }
    </div>);
}

// `browserHistory` will keep track of the current browser URL
let router = (
  <Router history={browserHistory}>
    <Route path="/" component={App}>
      <IndexRoute component={Home} />
    </Route>
  </Router>
);

import App from './components/App.jsx';
import Home from './components/Home';
import About from './components/About';
import NotFound from './components/NotFound';
import TweetSearch from './components/twitter/TweetSearch';

let router = (
  <Router history={browserHistory}>
    <Route name="twitter-searcher" path="/" component={App}>
      <IndexRoute component={Home} />
      <Route path="about" component={About} />
      <Route path="search" component={TweetSearch} />
      <Route path="*" component={NotFound} />
    </Route>
  </Router>
);

Rendering with Routes on the Client

  • we have a heavy client, a single page app
  • routing will take place on the client
  • browserHistory will:
    • intercept browser URL
    • render correct component
    • without sending new request to server

We define all components, routes and the router on the client, as shown before.

Then we render the correct route into HTML like so:

// after important all top-level components and setting up router...

// Use current URL to render correct components.
// Inject rendered components into the 'app-view' HTML DOM element.

render(router, document.getElementById('app-view'));

Summary

Client does almost everything.

  • all React components/elems/routes defined on browser
  • components run business logic that's defined on client
    • using imported libraries
  • this code will be bundled into one JavaScript file
  • browser requests that one file
  • now to create that web server which provides that file

2. Serving Application

  • JavaScript Package Manager
  • over 250,000 packages
  • manages metadata on your JS projects, such as:
    • project's name, purpose and author
    • execution and test commands
    • dependencies

Generate JavaScript Project

  • npm init
  • generates package.json file that stores project metadata
  • all node projects use this file

Install Dependencies

npm install --save react react-router

adds dependencies to package.json.

Serving the React App

Web Pack

  • webpack module bundler
  • compiles all ES6/JSX modules into browser-compatible JS
  • compiles React app into a single static asset
  • bundle.js
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Twitter Searcher</title>
  </head>
  <body>
    <div id="app-view"></div>
    <script type="application/javascript" src="/assets/bundle.js">
    </script>
  </body>
</html>

Development

npm install -g webpack webpack-dev-server
webpack-dev-server --progress --colors
  • binds small express server to localhost:8080
  • watches project files and automatically recompiles bundle when changes are detected
  • page at localhost:8080 automatically updates

Production Deployment

npm install -g webpack
webpack --progress --color -p --config webpack.prod.config.js
  • build webpack bundle with production config
  • e.g. optimised/minified JavaScript
  • entire app can now be served as static files:
    • index.html, bundle.js, stylesheets and images

Webpack Requirements

  • Babel plugins to compile ES6 / JSX
    • npm install babel-loader --save
    • .babelrc file to specify compiler options
  • webpack.config.js
  • likely a production config as well

Isomorphic Support

  • we want to make this an isomorphic app
  • means server will render the full page in its initial state
  • just using webpack's bundle.js means client does everything

Let's use a small HTTP server for this.

Express

Fast, unopinionated, minimalist web framework for node.

  • lightweight HTTP server library
  • defines API endpoints using URL routes
    • e.g. /auth/login
  • defines request handling logic for each endpoint

Simple HTTP Server

npm install --save express body-parser
const express = require('express');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json()); // for parsing application/json

app.post('/hello', function (req, res) {
  res.json({
    message: 'Hello ' + req.body.name + '!'
  });
});

app.listen(8080);

What Should the Server Do?

  • intercept all endpoints
  • use endpoint to determine react route
  • render page in the route's initial state
  • return rendered HTML
// create express app

import { renderToString } from 'react-dom/server';
import { RouterContext, match } from 'react-router';
import createLocation from 'history/lib/createLocation';
import routes from 'routes'; // shared app routes

// Intercept all routes!
app.use((req, res) => {

  // Take endpoint the client used and resolve it into react-router location
  const location = createLocation(req.url);

  // Attempt to match location to one of the app's routes
  match({ routes, location }, (err, redirectLocation, renderProps) => {
    // [ HANDLE ERRORS ]

    // render initial view of the routes into HTML
    const InitialView = <RouterContext {...renderProps} />;
    const componentHTML = renderToString(InitialView);

    // Inject rendered HTML into a shell document and return that to client
    res.status(200).end(`
      <!DOCTYPE html>
      <html>
        <head> <meta charset="UTF-8"> <title>Twitter Searcher</title> </head>
        <body>
<div id="app-view">${componentHTML}</div>
<script type="application/javascript" src="/assets/bundle.js">
</script>
        </body>
      </html>
    `);
  });
});
// Define static assets to serve clients.
// Just serve files from local directory for now.
const assetPath = path.join(__dirname, '..', 'assets');
app.use('/assets', express.static(assetPath));

Summary

  • bundle React web application into bundle.js file
    • webpack
  • server file statically
  • create small web server to render initial state of page
    • and serve static bundle.js file

3. RESTful § API

Will search Twitter for tweets matching user queries

twitterSearchApi

  • node service with single endpoint:
    • /api/search — search for tweets matching supplied query
  • will search using Twitter's own search API
  • service is just a thin wrapper around Twitter's API

Another Express Service

import express from 'express';
import bodyParser from 'body-parser';

const app = express();
app.use(bodyParser.json());

app.use('/api/search', require('./routes/search'));

const port = process.env.PORT || 8080;
app.listen(port, function() {
    console.log('Started twittersearchapi on port #{port}')
});

/api/search

import { Router } from 'express';
import Config from '../../config.js';

const router = Router();

function search(query) {
  return new Promise(function(resolve, reject) {
    // twitter search API calls go here
  });
}

router.post('/', function(req, res) {
  if (query) {
    search(query).then(function(tweets) {
      res.status(200).json({ tweets: tweets });
    }, function (err) {
      res.status(500).json({ error: err });
    });
  } else {
    res.status(400).json({ error: "No query specified" });
  }
});

export default router;

Call API on Browser Client

import axios from 'axios';

// put URL in config
const API_URL = 'http://127.0.0.1:8080/api/search';
const reqBody = { query: "@DonaldWhyte" };

axios.post(API_URL, reqBody).then(
  function(response) {
    // Log each returned tweet
    response.tweets.forEach(function(t) {
      console.log(JSON.stringify(t));
    });
  },
  function(err) {
    console.error('Error:', err);
  }
);

Twitter Searcher App

shared/services/twitter.js:

import { searchTweets } from '../../services/twitter';

export default class TweetSearch extends React.Component {

  // called whenever text entry value changes
  onQueryChange = (query) => {
    searchTweets(query).then(
      function(tweets) {
        this.setState({     // triggers re-render with new tweets
          tweets: tweets
        });
      },
      function(err) {
        console.error('Failed to search Twitter:', err);
        this.setState({     // triggers re-render with no tweet
          tweets: []
        });
      }
    );
  }

Conclusion

Single page applications are starting to become the norm for rich web applications.

However, SAPs have their problems.

Isomorphic applications are a middle-ground

Render initial page on the server, then let the client take over

Requires ability to write UI code once and have it run everywhere

React is a JavaScript-based UI framework.

Build components which manages a specific widget on the screen render elements on page and also manage.

Components are isolated, reusable and testable units, whose details are abstracted from the real browser DOM.

Deploy apps by bundling them in a single static JS file:

webpack --progress --color -p --config webpack.prod.config.js

Serve using bootstrap HTML:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Twitter Searcher</title>
</head>
<body>
  <div id="app-view"></div>
  <script type="application/javascript" src="/assets/bundle.js">
  </script>
</body>
</html>

Render initial page state on server for isomorphism:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Twitter Searcher</title>
</head>
<body>
  <div id="app-view">${ initialPageHTML }</div>
  <script type="application/javascript" src="/assets/bundle.js">
  </script>
</body>
</html>

Build small, single purpose APIs for your app to use.

Node / Express

RESTful API that backs the React app

Uses Twitter Search API to search for tweets using queries specified on the app

Find the slides and code here

Additional Topics

Questions?

Building Isomorphic Web Applications Using React and Node Created by Donald Whyte / @donald_whyte