Server Rendering
Alright, first things first. Server rendering, at its core is a simple concept in React.
render(<App/>, domNode)
// can be rendered on the server as
const markup = renderToString(<App/>)
It's not rocket science, but it also isn't trivial. First I'm going to just throw a bunch of webpack shenanigans at you with little explanation, then we'll talk about the Router.
Since node doesn't (and shouldn't) understand JSX, we need to compile
the code somehow. Using something like babel/register
is not fit for
production use, so we'll use webpack to build a server bundle, just like
we use it to build a client bundle.
Make a new file called webpack.server.config.js
and put this stuff in
there:
var fs = require('fs')
var path = require('path')
module.exports = {
entry: path.resolve(__dirname, 'server.js'),
output: {
filename: 'server.bundle.js'
},
target: 'node',
// keep node_module paths out of the bundle
externals: fs.readdirSync(path.resolve(__dirname, 'node_modules')).concat([
'react-dom/server'
]).reduce(function (ext, mod) {
ext[mod] = 'commonjs ' + mod
return ext
}, {}),
node: {
__filename: true,
__dirname: true
},
module: {
loaders: [
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader?presets[]=es2015&presets[]=react' }
]
}
}
Hopefully some of that makes sense, we aren't going to cover what all of
that stuff does, it's sufficient to say that now we can run our
server.js
file through webpack and then run it.
Now we need to make some scripts to build server bundle before we try to
run our app. Update your package.json
script config to look like
this:
"scripts": {
"start": "if-env NODE_ENV=production && npm run start:prod || npm run start:dev",
"start:dev": "webpack-dev-server --inline --content-base public/ --history-api-fallback",
"start:prod": "npm run build && node server.bundle.js",
"build:client": "webpack",
"build:server": "webpack --config webpack.server.config.js",
"build": "npm run build:client && npm run build:server"
},
Now when we run NODE_ENV=production npm start
both the client and
server bundles get created by Webpack.
Okay, let's talk about the Router. We're going to need our routes split
out into a module so that both the client and server entries can require
it. Make a file at modules/routes
and move your routes and components
into it.
// modules/routes.js
import React from 'react'
import { Route, IndexRoute } from 'react-router'
import App from './App'
import About from './About'
import Repos from './Repos'
import Repo from './Repo'
import Home from './Home'
module.exports = (
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="/repos" component={Repos}>
<Route path="/repos/:userName/:repoName" component={Repo}/>
</Route>
<Route path="/about" component={About}/>
</Route>
)
// index.js
import React from 'react'
import { render } from 'react-dom'
import { Router, browserHistory } from 'react-router'
// import routes and pass them into <Router/>
import routes from './modules/routes'
render(
<Router routes={routes} history={browserHistory}/>,
document.getElementById('app')
)
Now open up server.js
. We're going to bring in two modules from React
Router to help us render on the server.
If we tried to render a <Router/>
on the server like we do in the
client, we'd get an empty screen since server rendering is synchronous
and route matching is asynchronous.
Also, most apps will want to use the router to help them load data, so asynchronous routes or not, you'll want to know what screens are going to render before you actually render so you can use that information to load asynchronous data before rendering. We don't have any data loading in this app, but you'll see where it could happen.
First we import match
and RouterContext
from react router, then
we'll match the routes to the url, and finally render.
// ...
// import some new stuff
import React from 'react'
// we'll use this to render our app to an html string
import { renderToString } from 'react-dom/server'
// and these to match the url to routes and then render
import { match, RouterContext } from 'react-router'
import routes from './modules/routes'
// ...
// send all requests to index.html so browserHistory works
app.get('*', (req, res) => {
// match the routes to the url
match({ routes: routes, location: req.url }, (err, redirect, props) => {
// `RouterContext` is the what `Router` renders. `Router` keeps these
// `props` in its state as it listens to `browserHistory`. But on the
// server our app is stateless, so we need to use `match` to
// get these props before rendering.
const appHtml = renderToString(<RouterContext {...props}/>)
// dump the HTML into a template, lots of ways to do this, but none are
// really influenced by React Router, so we're just using a little
// function, `renderPage`
res.send(renderPage(appHtml))
})
})
function renderPage(appHtml) {
return `
<!doctype html public="storage">
<html>
<meta charset=utf-8/>
<title>My First React Router App</title>
<link rel=stylesheet href=/index.css>
<div id=app>${appHtml}</div>
<script src="/bundle.js"></script>
`
}
var PORT = process.env.PORT || 8080
app.listen(PORT, function() {
console.log('Production Express server running at localhost:' + PORT)
})
And that's it. Now if you run NODE_ENV=production npm start
and visit
the app, you can view source and see that the server is sending down our
app to the browser. As you click around, you'll notice the client app
has taken over and doesn't make requests to the server for UI. Pretty
cool yeah?!
Our callback to match is a little naive, here's what a production version would look like:
app.get('*', (req, res) => {
match({ routes: routes, location: req.url }, (err, redirect, props) => {
// in here we can make some decisions all at once
if (err) {
// there was an error somewhere during route matching
res.status(500).send(err.message)
} else if (redirect) {
// we haven't talked about `onEnter` hooks on routes, but before a
// route is entered, it can redirect. Here we handle on the server.
res.redirect(redirect.pathname + redirect.search)
} else if (props) {
// if we got props then we matched a route and can render
const appHtml = renderToString(<RouterContext {...props}/>)
res.send(renderPage(appHtml))
} else {
// no errors, no redirect, we just didn't match anything
res.status(404).send('Not Found')
}
})
})
Server rendering is really new. There aren't really "best practices" yet, especially when it comes to data loading, so this tutorial is done, dropping you off at the bleeding edge.