Server-Side Rendering with React and React-Router


8 minute read


update: This post was last updated on 4/16/16 w/ more-recent react-router.

tl;dr: React is the only framework you ever need to learn again! Proclaim it’s superiority from the mountaintops! No longer will we be trapped in the chains of Angular, Ember, Backbone…REACT IS FREEDOM. Eh…Jk. React is really great, but please don’t be that person. We need to love all the frameworks equally, lest we just keep reinventing them over and over and over…and over again ;) I’m way off track of this tl;dr now.

*To point: taking advantage of node running on the server and React’s ability to RenderToString() give you a flexible, elegant, and isomorphic universal approach to rendering and running your react app. This is just one approach to server-side rendering with React that I’ve found to be flexible and very helpful. I’d love to hear other approaches people have taken!*

Back to Server-Side Rendering – Really?!

Yeah, really. We’re going to render on the server like it’s PHPconf 2001 (disclaimer: we won’t be making any use of The PHP™ ; get down from the ledge). But what about all the glorious freedom we’ve had from serverland?

First, serverland is a great place to go. The snacks are good, the rides are fun, there’s even an Ubuntu waterslide. Second, don’t worry – we’re not really going to be spending too much time in serverland. Just enough to let React do it’s magic and then we’re back in clientland.

We’re going to do server-side rendering with the magic of React. As of the time of writing ([email protected]), React gives us a few different ways to render the virtual DOM elements that it creates and manages for us:

Using react-dom (react-dom/server), which was broken out of the main React module to better separate concerns:

  1. renderToString(ReactElement element) -> string: In short, creates all the html we need on the server. More fully, from the React docs:

Render a ReactElement to its initial HTML. This should only be used on the server. React will return an HTML string. You can use this method to generate HTML on the server and send the markup down on the initial request for faster page loads and to allow search engines to crawl your pages for SEO purposes.

If you call ReactDOM.render() on a node that already has this server-rendered markup, React will preserve it and only attach event handlers, allowing you to have a very performant first-load experience.

  1. renderToStaticMarkup(ReactElement element) -> string: In short, the less cool sibling of renderToString. Again, from the docs: > Similar to renderToString, except this doesn’t create extra DOM attributes such as data-react-id, that React uses internally. This is useful if you want to use React as a simple static page generator, as stripping away the extra attributes can save lots of bytes.

So, we have the ability to now send down the react-ified version of our markup from a server to the client. Now, add in the power of [react-router](https://github.com/rackt/react-router) and you have a potent combo. Hopefully you're nodding your head right around now and starting to see the potential for a setup that uses this approach.

Are you with me?!

yes, I am with you. Show me the magic.

##Upsides

In case you haven’t been instantly enlightened with all the benefits of server-side rendering with react and node, here are a few that I’ve found in my experience.

- (Potentially) Increased Server Efficiency: Perhaps the best benefit of this approach is the decreased server load and seamless app handoff that takes place between server & client. You can give your users something to see while they wait for your bundled and minified JS app to download, and once its done, React takes over and the user doesn’t have to hit the server again to get assets or page content — that’s all handled through a RESTful JSON API. This means less rendering requests to the server and potentially (a lot) less load on your server!

  • SEO-friendliness: Even though Google can already execute JS in it’s crawlers, this is pretty much just a safer bet and/or fallback for non-js situations. If your app script is loading slowly, you can still render the actual page to your client and not make them wait while staring at a blank screen. This also allows someone with JS disabled on their browser to still interact with your app for the most part; links will still work, forms can still submit, &c. Some will note that since google can already execute JS in-crawler, this is superfluous, but people in the community still seem to experience superior SEO benefits.

  • Code-sharing between the client and server: There’s nothing necessarily incredible about this aside from the fact that complexity & duplication are decreased and, as such, you get all the benefits therein (potentially less coupling, easier maintainability, greater simplicity in structure, isomorphic-ness, &c.)

  • (spoiler alert) No more hash fragments: A further side benefit of the approach we’ll look at is the ability to use react-router’s HTML5 history API instead of the annoying hash-fragment approach you have to use in other cases. This is generally because you’re not performing full-cycle page requests when you transition between, say, Angular views like you normally would to move between pages.

  • Progressive Enhancement: Aside from showing the user something instead of a white screen, you have the ability to progressively enhance their experience while a static (but working!) document is first rendered and then react can take over. Hopefully your actual app script is small, but if it’s not a couple KB (not including React), you can at least rest assured you won’t be sending just white screens to users everywhere. This matters even more on mobile, where bandwidth and processor resources are a premium.

Make It So!

And now to the implementation details. For those who aren’t aware of how react can be used with server-side rendering, it’s fairly straightforward: Node can be used to execute React methods. React’s renderToString() method on a React app and then send that to the requesting client. In theory, you could even use React as just a static page generator in this way, but that’s pretty boring.

The basic approach I’ve been using goes roughly as follows:

  1. Upon bootstrap/startup, the node app instantiates a react-router instance based on routes.jsx (e.g., the routes that you have set up using react-router)
  2. A request goes to the server, which then uses express’ req.path to provide a route string for react-router to handle.
  3. React-router then matches the provided route and renders the corresponding component(s) for express to send back.
  4. Express sends down the React-generated HTML response and your client gets to paint something regardless of the speed of your app script download. We serve ours over a great CDN, but even with the best distribution and compression slow networks would still otherwise leave people with a temporarily blank screen.
  5. Having loaded the app script, React can use the same routes.jsx along with react-router file to take over and manage the DOM from here on out.

Let’s just take a moment to toast to React, Node, and React-Router for making all this possible:

Here’s to you, JavaScript.

##le Code:

Browser.jsx

import ReactDOM from 'react-dom';
import { Router } from 'react-router';
import { Routes } from './routes';

ReactDOM.render(
   <Router>
      <Routes>
   </Router>,
   document);

App.js (express server):

//...other express configuration

import { renderToString } from 'react-dom/server'
import { match, RouterContext } from 'react-router'
import routes from './routes'

serve((req, res) => {
  // Note that req.url here should be the full URL path from
  // the original request, including the query string.
  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) {
      // You can also check renderProps.components or renderProps.routes for
      // your "not found" component or route respectively, and send a 404 as
      // below, if you're using a catch-all route.
      res.status(200).send(renderToString(<RouterContext {...renderProps} />))
    } else {
      res.status(404).send('Not found')
    }
  })
})
});

//...other express configuration

Routes.jsx

import {Route, IndexRoute} from 'react-router';

const Routes = <Route path="/" component={App}>
  <IndexRoute component={Welcome}/>
  <Route path="dashboard" handler={Dashboard}/>
  <Route path="login" component={Login}/>
</Route>

export Routes;

App.jsx

//...import components up here
//... and return the below w/in a top-level component

<Navigation/>
<RouteHandler/>
<Footer/>

One more point worth noting: I use browserify webpack to bundle my react code and now browser.jsx is the entry point. Before refactoring for server-side rendering it was previously app.jsx; you might need to re-configure your app structure to accommodate what gets rendered where.

Cautionary Notes

I’ve been working a version of this approach for a good bit of time now and it has proved quite resilient. It’s made it possible to use lower-powered server instances because so many fewer requests need to be made during a user session. Additionally, offloading most asset hosting to CDNs has made it so all my app server really has to do is render and send the react app to the client, where it’ll take over and hit a JSON API for everything else.

However, there are a couple gotchas to be very aware of. We need to remember that node has no notion of a document or window object to use. React-dom seems to solve this problem, but if you’re using third-party components you need to watch out for third-party components that haven’t been updated in some time.

One solution to this has to do with how React works server-side. You should limit any sort of rendering/state-setting calls to componentDidMount() instead of componentWillMount() to avoid a bug(ish?) issue in v0.13.3. This appears to have been fixed, but is worth pointing out for the future.

Happy rendering!

¯\(ツ)

See [react/issues/3620] Thoughts, questions, ideas, tirades? Write them all in the comments below or discuss them on Hacker News!

Related: