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
Tweet

More Decks by Jack Franklin

Other Decks in Programming

Transcript

  1. "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."
  2. "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"
  3. 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
  4. 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!
  5. A standard React app: class MyApp extends React.Component { render()

    { ... } } ReactDOM.render( <MyApp />, document.getElementById('app') )
  6. // 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 }); });
  7. 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
  8. 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
  9. There's no requirement to actually go client side A server

    side generated React app with no client side JS is perfectly fine ;)
  10. 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>
  11. Creating client.js: import React from 'react'; import ReactDOM from 'react-dom';

    import MyApp from './component'; ReactDOM.render( <MyApp />, document.getElementById('app') );
  12. 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' } ] } }
  13. 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)
  14. 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> ); } }
  15. 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?
  16. 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.
  17. 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> ); } }
  18. 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> );
  19. Match against the URL on the server. Gets a bit

    hairy, stick with me! React Router server side guide
  20. // 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';
  21. 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') } }) });
  22. // 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 ... });
  23. 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) { ... }
  24. ... } else if (redirectLocation) { // if we need

    to redirect, redirect to the new URL res.redirect(302, redirectLocation.pathname + redirectLocation.search) } else if (renderProps) { ... }
  25. ... } 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 { ... }
  26. } else { // if we get here, it's not

    an error, redirect or match // hence, 404! res.status(404).send('Not found') }
  27. components/about.js: import React from 'react'; export default class AboutComponent extends

    React.Component { render() { return <p>Rockstar developer</p>; } }
  28. 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> );
  29. 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> ); } }
  30. 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.
  31. (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!
  32. 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
  33. Caveat 2: Async-Props and React Router 2 don't play 100%

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

    npm install --save isomorphic-fetch
  35. 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.
  36. 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> ); } }
  37. 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 }); }); }
  38. • 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>
  39. 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)
  40. Deep Breath These slides and all the demos are on

    GitHub. Please send me questions: @Jack_Franklin or [email protected].
  41. Universal JavaScript is here to stay. The techniques, libraries and

    approaches will change over time. There's plenty to figure out in this space!
  42. Further Reading • Universal React on 24ways • How we

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

    more! Day long React workshops in London: • 16 March • 10 June