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

Universal React

Universal React

My explorations in universal React.


Jack Franklin

February 04, 2016


  1. Universal React

  2. @Jack_Franklin, Pusher

  3. None
  4. None
  5. Why?

  6. Source: GDS Blog, 2013

  7. "Surprisingly, the proportion of people that have explicitly disabled JavaScript

    or use a browser that doesn't support JavaScript, only makes up a small slice of people that don't run JavaScript."
  8. "Progressive enhancement has never been about users who've turned JavaScript

    off, or least it wasn't for me." Jake Archibald, "Progressive Enhancement Still Important"
  9. Everyone has JS, right? • On a train / in

    a tunnel / etc • HTTP request hangs • Firewalls • ISP is interfering • A browser addon is messing with you • Your CDN is down Stuart Langridge's Flowchart
  10. Time and a place

  11. Cutting Edge! We're really still at the early stages of

    figuring out how this stuff works. Some of the code shown here isn't the easiest, or the APIs aren't that straight forward. This will change as we learn more. Don't expect this to be 100% smooth!
  12. A standard React app: class MyApp extends React.Component { render()

    { ... } } ReactDOM.render( <MyApp />, document.getElementById('app') )
  13. Server side: class MyApp extends React.Component { render() { ...

    } } ReactDOM.renderToString(<MyApp />)
  14. React is a great fit

  15. None
  16. Demo One! Rendering HTML on the server from a React

  17. // some imports left out to save space import React

    from 'react'; import MyApp from './component'; import { renderToString } from 'react-dom/server'; const app = express(); app.get('*', (req, res) => { const markup = renderToString(<MyApp />); res.render('index', { markup }); });
  18. renderToString When your HTML is going to be picked up

    by React on the client renderToStaticMarkup When your HTML is never going to be edited by client-side React
  19. Going Client side • Shared set of components that are

    environment agnostic • A server rendering step (like we just saw) • A client rendering step • A bundler to generate our client side JavaScript
  20. There's no requirement to actually go client side A server

    side generated React app with no client side JS is perfectly fine ;)
  21. Agnostic Components

  22. Webpack A JavaScript application bundler that will generate our client

    side build.
  23. Rendering on the Client

  24. Updating our server template. <body> <!-- rendering React into body

    is bad --> <!-- lack of whitespace here is really important! --> <div id="app"><%- markup %></div> <script src="build.js"></script> </body>
  25. Creating client.js: import React from 'react'; import ReactDOM from 'react-dom';

    import MyApp from './component'; ReactDOM.render( <MyApp />, document.getElementById('app') );
  26. Same Components on client and server

  27. npm install --save-dev webpack babel-loader

  28. Create webpack.config.js var path = require('path'); module.exports = { entry:

    path.join(process.cwd(), 'client.js'), output: { path: './public/', filename: 'build.js' }, module: { loaders: [ { test: /.js$/, loader: 'babel' } ] } }
  29. Run webpack: > webpack Hash: 78c865d5593fe910f823 Version: webpack 1.12.12 Time:

    4948ms Asset Size Chunks Chunk Names build.js 690 kB 0 [emitted] main + 160 hidden modules (Tip: webpack -w for continuous rebuilds)
  30. An interactive component export default class MyApp extends React.Component {

    constructor() { super(); this.state = { count: 0 }; } onClick() { this.setState({ count: this.state.count + 1 }); } render() { return ( <div> <button onClick={this.onClick.bind(this)}>Click Me</button> <p>Count: { this.state.count }</p> </div> ); } }
  31. Demo Two! (Contrived example incoming!)

  32. Most web apps still do a lot of rendering of

    data (from APIs, etc) and why would you prevent your non-JS users from that?
  33. A read-only experience is way better than no experience.

  34. Routing

  35. Don't be that person who breaks the web.

  36. react-router The defacto, practically standard routing solution for React. https://github.com/rackt/react-router

  37. react-router 2.0.0-rc5 We're living on the bleeding edge here! React

    Router 2.0 upgrade guide
  38. First we need some more components, starting with components/app.js: import

    React from 'react'; export default class AppComponent extends React.Component { render() { return ( <div> <h2>My web 2.0 app</h2> { this.props.children } </div> ); } } this.props.children are the nested routes.
  39. And then components/index.js: import React from 'react'; export default class

    IndexComponent extends React.Component { render() { return ( <div> <p>This is the index page</p> </div> ); } }
  40. Define our routes: import { Route } from 'react-router'; import

    React from 'react'; import AppComponent from './components/app'; import IndexComponent from './components/index'; export const routes = ( <Route path="" component={AppComponent}> <Route path="/" component={IndexComponent} /> </Route> );
  41. Match against the URL on the server. Gets a bit

    hairy, stick with me! React Router server side guide
  42. // our newly defined routes import { routes } from

    './routes'; // match is responsible for matching routes against a URL // RouterContext renders the components in the matched routes import { match, RouterContext } from 'react-router';
  43. app.get('*', (req, res) => { match({ routes, location: req.url },

    (error, redirectLocation, renderProps) => { if (error) { res.status(500).send(error.message) } else if (redirectLocation) { res.redirect(302, redirectLocation.pathname + redirectLocation.search) } else if (renderProps) { res.render('index', { markup: renderToString(<RouterContext {...renderProps} />) }); } else { res.status(404).send('Not found') } }) });
  44. Let's break that down...

  45. // take our app's routes, and the URL of the

    request match({ routes, location: req.url }, (error, redirectLocation, renderProps) => { // match figures out which routes match, and calls this callback with the arguments above // error : given if something went wrong matching a route // redirectLocation : returned if the URL matches a redirect // renderProps : given if a route was matched and we can render ... });
  46. if (error) { // if there was an error, 500

    with the error message // you might show a custom error HTML page here res.status(500).send(error.message) } else if (redirectLocation) { ... }
  47. ... } else if (redirectLocation) { // if we need

    to redirect, redirect to the new URL res.redirect(302, redirectLocation.pathname + redirectLocation.search) } else if (renderProps) { ... }
  48. ... } else if (renderProps) { // if we have

    renderProps that means we have a match and can render res.render('index', { // RouterContext is React Router's wrapper around our own components // and renderProps contains all the info React Router needs to render our app markup: renderToString(<RouterContext {...renderProps} />) }); } else { ... }
  49. } else { // if we get here, it's not

    an error, redirect or match // hence, 404! res.status(404).send('Not found') }
  50. None
  51. Let's add an about page!

  52. components/about.js: import React from 'react'; export default class AboutComponent extends

    React.Component { render() { return <p>Rockstar developer</p>; } }
  53. routes.js: import AppComponent from './components/app'; import IndexComponent from './components/index'; import

    AboutComponent from './components/about'; import React from 'react'; import { Route } from 'react-router'; export const routes = ( <Route path="" component={AppComponent}> <Route path="/" component={IndexComponent} /> <Route path="/about" component={AboutComponent} /> </Route> );
  54. And some links... import React from 'react'; import { Link

    } from 'react-router'; export default class AppComponent extends React.Component { render() { return ( <div> <h2>My web 2.0 app</h2> <Link to="/">Home</Link> <Link to="/about">About</Link> { this.props.children } </div> ); } }
  55. With no client side bundle, this works perfectly:

  56. Updating the client side: import React from 'react'; import ReactDOM

    from 'react-dom'; import { Router, browserHistory } from 'react-router'; import { routes } from './routes'; ReactDOM.render( <Router routes={routes} history={browserHistory} />, document.getElementById('app') ) And then rerun webpack.
  57. Demo Three!

  58. Dealing with Data (If I have time...)

  59. (Caveat: this area is still WIP) No one has quite

    figured out the best way to deal with loading data on the server and client with React components. This is very, very new terrirory and things are not settled yet. This will get a little messy!
  60. I'm a fan of Async Props (also by the creator's

    of React Router). Warning: Async Props is not production ready yet, but I like the approach enough to consider it safe to demo. npm install --save async-props
  61. Caveat 2: Async-Props and React Router 2 don't play 100%

    nicely So for this demo I've downgraded to React Router 1.
  62. Going to use the fetch API, so need a polyfill:

    npm install --save isomorphic-fetch
  63. AsyncProps expects a loadProps static method in your Read component

    that loads the data. All we need to do is define it, and AsyncProps will take care of the rest. We can then refer to that data on this.props without worrying about if it's loaded or not, because AsyncProps ensures that it is.
  64. First, let's give components/index.js some data: export default class IndexComponent

    extends React.Component { // a stage 1 proposal for ES.next static loadProps(params, cb) { fetch('https://api.github.com/users/jackfranklin').then((data) => { return data.json(); }).then((github) => { cb(null, { github }); }).catch((e) => { cb(e); }); } render() { return ( <div> <p>My github repo count: { this.props.github.public_repos }</p> </div> ); } }
  65. Then we update our server rendering: // within the `match`

    callback: } else if (renderProps) { // loadPropsonServer is provided by async-props loadPropsOnServer(renderProps, (err, asyncProps, scriptTag) => { res.render('index', { // we now render AsyncProps markup: renderToString(<AsyncProps {...renderProps} {...asyncProps} />), scriptTag }); }); }
  66. • loadPropsOnServer gives us two items in the callback: asyncProps

    and scriptTag. • The scriptTag is needed to transfer the data from server to client. We change our index.ejs template: <div id="app"><%- markup %></div> <%- scriptTag %> <script src="build.js"></script>
  67. And finally we can update our client side rendering too:

    import AsyncProps from 'async-props'; ReactDOM.render( <Router routes={routes} history={createBrowserHistory()} RoutingContext={AsyncProps} />, document.getElementById('app') ) (Note: this looks much nicer in React Router v2)
  68. Demo 4!

  69. Deep Breath These slides and all the demos are on

    GitHub. Please send me questions: @Jack_Franklin or jack@pusher.com.
  70. Universal JavaScript is here to stay. The techniques, libraries and

    approaches will change over time. There's plenty to figure out in this space!
  71. Server side by default?

  72. Further Reading • Universal React on 24ways • How we

    built the new gocardless.com • GoCardless.com repo • Universal React Example app
  73. If you dig React I can make you dig it

    more! Day long React workshops in London: • 16 March • 10 June
  74. Thanks! @Jack_Franklin http://speakerdeck.com/jackfranklin