"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."
"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"
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
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!
// 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(); res.render('index', { markup }); });
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
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
First we need some more components, starting with components/app.js: import React from 'react'; export default class AppComponent extends React.Component { render() { return (
// 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';
// 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 ... });
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) { ... }
... } else if (redirectLocation) { // if we need to redirect, redirect to the new URL res.redirect(302, redirectLocation.pathname + redirectLocation.search) } else if (renderProps) { ... }
... } 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() }); } else { ... }
And some links... import React from 'react'; import { Link } from 'react-router'; export default class AppComponent extends React.Component { render() { return (
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( , document.getElementById('app') ) And then rerun webpack.
(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!
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
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.
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(), scriptTag }); }); }
• 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:
And finally we can update our client side rendering too: import AsyncProps from 'async-props'; ReactDOM.render( routes={routes} history={createBrowserHistory()} RoutingContext={AsyncProps} />, document.getElementById('app') ) (Note: this looks much nicer in React Router v2)