Building Isomorphic Web Applications

Using React and Node

Created by Donald Whyte / @donald_whyte

logo
### 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 ![arch_mainframe](images/arch_mainframe.svg)
### Desktop Clients > Heavy client, thin server ![arch_desktops](images/arch_desktops.svg)
### Birth of the Web > Thin client, heavy server ![arch_clientserver](images/arch_clientserver.svg)
### Single Page Application > Heavy client, thin server ![arch_wsap](images/arch_sap.svg)
```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> ```
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
![client_side_sap](images/client_side_sap.png)
![isomorphic_sap](images/isomorphic_sap.png)
## The Stack
### Web App (Client & Server) Many popular frameworks: * AngularJs * Meteor * Backbone * React * etc. **React** will be used for this workshop
### React ![react](images/react.svg) * 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 ![mvc](images/mvc.svg)
### 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!
![nodejs](images/nodejs.svg) * 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) ![javascript](images/javascript.svg) * **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 ```js class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `(${this.x}, ${this.y})`; } } ```
### Inheritence ```js 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. ```js // 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: ```js export function foobar(num) { console.log('FOOBAR:', num); } ```
### Exporting Entire Objects person.js: ```js export default { name: 'Donald', age: 24 }; ``` another_file.js ``` import person as './person'; console.log(person.name); // outputs 'Donald' ```
### Promises [es6fiddle](http://www.es6fiddle.net/inufuuas/) ```js 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 ![react_row](images/react.svg) ![nodejs_row](images/nodejs.svg) ![javascript_row](images/javascript.svg)
## Let's Build Something!
### Twitter Search Engine
![twitter_search_screenshot](images/twitter_search_screenshot.png)
## 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 ![twitter_search_architecture](images/twitter_search_architecture.svg)
### Steps 1. Build single page application 2. Serve it as an isomorphic app 3. Implement `twitterSearchApi`
## 1. Single Page Application ![react_title](images/react.svg)
### The DOM ![dom](images/dom.svg)
### 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
![virtual_dom_diff](images/virtual_dom_diff.png)
### Building Blocks * `ReactElement` * `ReactNode` * `ReactComponent`
`ReactElement` — [jsfiddle](http://jsfiddle.net/tf55qk5f/) Lowest type in the virtual DOM, similar to an XML element. These can represent complex UI components. ```js 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 `ReactNode`s
![virtual_dom](images/virtual_dom.svg)
`ReactComponent` A specification on how to build `ReactElement`. `ReactElement`s are essentially *instantiations* of components.
[jsfiddle](http://jsfiddle.net/j5no6mpq/) ```html import React from 'react'; class Message extends React.Component { render() { return <div className='message'>{this.props.contents}</div>; } } ``` ```html import ReactDOM from 'react'; ReactDOM.render( <Message contents="Hello world!" />, document.getElementById('app-view'), function() { console.log('Callback that executes after rendering'); } ); ```
### wat? ```html 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) ```html <Message contents="Hello world!" /> ``` ##### Output (JavaScript) ```js React.createElement(Message, { contents: 'Hello world!' }); ```
### Nested Components ##### Input (JSX) ```html 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
```html 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](http://jsfiddle.net/gae7v9eh/) ```js 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_screenshot](images/twitter_search_screenshot.png)
![twitter_search_screenshot_search_split](images/twitter_search_screenshot_search_split.png) ![twitter_search_components](images/twitter_search_components.svg)
[Twitter Search Components](https://github.com/DonaldWhyte/isomorphic-react-workshop/tree/master/twitterSearchApp/shared/components/twitter)
Let's expand this model to an entire site
![twitter_app_components](images/twitter_app_components.svg)
### 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: ```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> ); ```
![twitter_router](images/twitter_router.svg)
```js 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: ```js // 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
![npm](images/npm.svg) * 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 ```bash npm install --save react react-router ``` adds dependencies to `package.json`.
### Serving the React App
![what_is_webpack](images/what-is-webpack.png)
### Web Pack ![webpack](images/webpack.svg) * webpack module bundler * compiles all ES6/JSX modules into browser-compatible JS * compiles React app into a single static asset * `bundle.js`
```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> ```
### Development ```bash 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 ```bash npm install --save express body-parser ``` ```js 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
```js // 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> `); }); }); ```
```js // 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 ```js 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 ```js 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 ```js 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](images/react.svg) 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: ```bash webpack --progress --color -p --config webpack.prod.config.js ``` Serve using bootstrap HTML: ```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. ![arch_wsap](images/arch_sap.svg)
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](https://github.com/DonaldWhyte/isomorphic-react-workshop)
### Additional Topics * [Why Do We Need Single Page Applications](http://stackoverflow.com/questions/16642259/why-do-we-need-a-single-page-application) * [Advantages/Disadvantages of SAPs](http://stackoverflow.com/questions/21862054/single-page-application-advantages-and-disadvantages) * [React Diffing Algorithm](http://calendar.perfplanet.com/2013/diff/) * [React Testing](https://facebook.github.io/react/docs/test-utils.html) * [9 Things Every React Beginner Should Know](https://camjackson.net/post/9-things-every-reactjs-beginner-should-know)
# Questions?