On Github DonaldWhyte / isomorphic-react-workshop
Created by Donald Whyte / @donald_whyte
Always good to try new things!
Some history...
Thin client, heavy server
Heavy client, thin server
Thin client, heavy server
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.
Many popular frameworks:
React will be used for this workshop
class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `(${this.x}, ${this.y})`; } }
class ColorPoint extends Point { constructor(x, y, color) { super(x, y); this.color = color; } toString() { return super.toString() + ' in ' + this.color; } }
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);
foo.js:
export function foobar(num) { console.log('FOOBAR:', num); }
person.js:
export default { name: 'Donald', age: 24 };
another_file.js
import person as './person'; console.log(person.name); // outputs 'Donald'
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);
DOM was never optimised for creating dynamic UIs
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
ReactComponent
A specification on how to build ReactElement.
ReactElements are essentially instantiations of components.
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'); } );
return <div className='message'>{this.props.contents}</div>;
<Message contents="Hello world!" />
React.createElement(Message, { contents: 'Hello world!' });
import config from './config'; let App = ( <Form endpoint={config.submitEndpoint}> <FormRow> <FormLabel text="Name" /> <FormInput /> </FormRow> <FormRow> <FormLabel text="Age" /> <FormInput /> </FormRow> </Form> );
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, {}), ] ), ] );
class TextEntry extends React.Component { render() { return <input type="text" value={this.props.initialValue} />; } }; React.render( <TextEntry initialValue="42" />, document.getElementById('container'));
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 and state both:
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...
Let's expand this model to an entire site
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> );
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'));
Client does almost everything.
npm install --save react react-router
adds dependencies to package.json.
<!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>
npm install -g webpack webpack-dev-server webpack-dev-server --progress --colors
npm install -g webpack webpack --progress --color -p --config webpack.prod.config.js
Let's use a small HTTP server for this.
Fast, unopinionated, minimalist web framework for node.
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);
// 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));
Will search Twitter for tweets matching user queries
twitterSearchApi
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;
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); } );
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: [] }); } ); }
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