Fluent Conf: Universal React

Fluent Conf: Universal React

Aea964cf59c0c81fff752896f070cbbb?s=128

Jack Franklin

March 10, 2016
Tweet

Transcript

  1. Universal JavaScript

  2. @Jack_Franklin, Pusher

  3. None
  4. None
  5. None
  6. None
  7. None
  8. Why?

  9. Source: GDS Blog, 2013

  10. "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."
  11. "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"
  12. 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
  13. Think about if this is right for you!

  14. 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!
  15. React paved the way

  16. None
  17. None
  18. Code and demos: https://github.com/jackfranklin/ universal-react-talk/tree/fluent-conf-version

  19. A standard React app: class MyApp extends React.Component { render()

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

    } } ReactDOM.renderToString(<MyApp />)
  21. // 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 }); });
  22. 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
  23. <!DOCTYPE html> <html> <head> <title>My App</title> </head> <body> <p data-reactid=".nbhys48o3k"

    data-react-checksum="-1312485142">Hello from React</p> </body> </html>
  24. 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
  25. There's no requirement to actually go client side A server

    side generated React app with no client side JS is perfectly fine.
  26. Agnostic Components

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

    side build.
  28. Rendering on the Client

  29. Updating our server template. <body> <!-- markup will be replaced

    with React generated HTML --> <div id="app"><%- markup %></div> <!-- build.js is our client side bundle --> <script src="build.js"></script> </body>
  30. Creating client.js: import React from 'react'; import ReactDOM from 'react-dom';

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

  32. npm install --save-dev webpack \ babel-loader \ babel-preset-es2015 \ babel-preset-react

    \ babel-yet-another-thing-lol-jk
  33. Create .babelrc { "presets": ["es2015", "react"] }

  34. 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$/, exclude: /node_modules/, loader: 'babel' } ] } }
  35. Run webpack: (PS: in production we'd minify, etc!) > 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)
  36. 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> ); } }
  37. None
  38. A read-only experience is way better than no experience.

  39. Routing

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

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

  42. 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.
  43. 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> ); } }
  44. 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> );
  45. Match against the URL on the server. Gets a bit

    hairy, stick with me! React Router server side guide
  46. // 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';
  47. 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') } }) });
  48. Let's break that down...

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

    request match({ routes, location: req.url }, (error, redirectLocation, renderProps) => { // 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 ... });
  50. 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) { ... }
  51. ... } else if (redirectLocation) { // if we need

    to redirect, redirect to the new URL res.redirect(302, redirectLocation.pathname + redirectLocation.search) } else if (renderProps) { ... }
  52. ... } 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 app // and renderProps contains all the info // React Router needs to render our app markup: renderToString(<RouterContext {...renderProps} />) }); } else { ... }
  53. } else { // if we get here, it's not

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

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

    React.Component { render() { return <p>Rockstar developer</p>; } }
  57. 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> );
  58. And some links... ... render() { return ( <div> <h2>My

    web 2.0 app</h2> <Link to="/">Home</Link> <Link to="/about">About</Link> { this.props.children } </div> ); } ...
  59. With no client side bundle, this works perfectly:

  60. 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.
  61. None
  62. Dealing with Data on the server and client

  63. (Caveat: this area is still WIP) Lots of unknowns!

  64. 1.We want to be able to fetch data on the

    server and/or on the client. 2.If the data is loaded on the server and rendered to the client, we ideally want to avoid making the request again.
  65. Async Props (also by the creator's of React Router). Not

    production ready, still a WIP. Check code/ with-async-data on GitHub for an example.
  66. 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) { fetchGithubData('jackfranklin').then((data) => { cb(null, { github: data }); }); } render() { return ( <div> <p>My github repo count: { this.props.github.public_repos }</p> </div> ); } }
  67. Then we update our server and client rendering. AsyncProps generates

    a script that will contain the fetched data from the server. The client can pick this data up and avoid having to make the request all over again.
  68. This is one of many approaches.

  69. React Resolver code/with-react-resolver has an example. class IndexComponent extends React.Component

    { render() { return ( <div> <p>This is the index page</p> <p>My github repo count: { this.props.github.public_repos }</p> </div> ); } } export default resolve('github', (props) => { return fetch('https://api.github.com/users/jackfranklin').then((data) => { return data.json(); }); })(IndexComponent);
  70. • Update the server side rendering to include window.__REACT_RESOLVER_PAYLOAD__. •

    Update the client side rendering to use React Resolver. • All data is resolved on the server, rendered and then used to populate data on the client.
  71. None
  72. This area is still being figured out - more solutions

    will definitely come!
  73. Code and Demos These slides and all the demos are

    on GitHub. jackfranklin/universal-react-talk Please send me questions: @Jack_Franklin or jack@pusher.com.
  74. Universal JavaScript is here to stay. The techniques, libraries and

    approaches will change over time. There's plenty to figure out in this space! Long term I expect frameworks to do more, and it become even easier for developers to take advantage.
  75. Server side by default?

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

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

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