116

I have been thinking and I am confused with the routing between Client and Server. Suppose I use ReactJS for server-side rendering before sending the request back to web browser, and use react-router as a client-side routing to switch between pages without refreshing as SPA.

What comes to mind is:

  • How are the routes interpreted? For example, a request from Home page (/home) to Posts page (/posts)
  • Where does the routing go, on server-side or client?
  • How does it know how it is processed?
Kenny John Jacob
  • 1,188
  • 8
  • 21
heartmon
  • 1,411
  • 2
  • 13
  • 14

2 Answers2

139

Note, this answer covers React Router version 0.13.x - the upcoming version 1.0 looks like it will have significantly different implementation details

Server

This is a minimal server.js with react-router:

var express = require('express')
var React = require('react')
var Router = require('react-router')

var routes = require('./routes')

var app = express()

// ...express config...

app.use(function(req, res, next) {
  var router = Router.create({location: req.url, routes: routes})
  router.run(function(Handler, state) {
    var html = React.renderToString(<Handler/>)
    return res.render('react_page', {html: html})
  })
})

Where the routes module exports a list of Routes:

var React = require('react')
var {DefaultRoute, NotFoundRoute, Route} = require('react-router')

module.exports = [
  <Route path="/" handler={require('./components/App')}>
    {/* ... */}
  </Route>
]

Every time a request is made to the server, you create a single-use Router instance configured with the incoming URL as its static location, which is resolved against the tree of routes to set up the appropriate matched routes, calling back with the top-level route handler to be rendered and a record of which child routes matched at each level. This is what's consulted when you use the <RouteHandler> component within a route handling component to render a child route which was matched.

If the user has JavaScript turned off, or it's being slow to load, any links they click on will hit the server again, which is resolved again as above.

Client

This is a minimal client.js with react-router (re-using the same routes module):

var React = require('react')
var Router = require('react-router')

var routes = require('./routes')

Router.run(routes, Router.HistoryLocation, function(Handler, state) {
  React.render(<Handler/>, document.body)
})

When you call Router.run(), it creates a Router instance for you behind the scenes, which is re-used every time you navigate around the app, as the URL can be dynamic on the client, as opposed to on the server where a single request has a fixed URL.

In this case, we're using the HistoryLocation, which uses the History API to make sure the right thing happens when you hit the back/forward button. There's also a HashLocation which changes the URL hash to make history entries and listens to the window.onhashchange event to trigger navigation.

When you use react-router's <Link> component, you give it a to prop which is the name of a route, plus any params and query data the route needs. The <a> rendered by this component has an onClick handler which ultimately calls router.transitionTo() on the router instance with the props you gave the link, which looks like this:

  /**
   * Transitions to the URL specified in the arguments by pushing
   * a new URL onto the history stack.
   */
  transitionTo: function (to, params, query) {
    var path = this.makePath(to, params, query);

    if (pendingTransition) {
      // Replace so pending location does not stay in history.
      location.replace(path);
    } else {
      location.push(path);
    }
  },

For a regular link this ultimately calls location.push() on whichever Location type you're using, which handles the details of setting up history so navigating with the back and forward buttons will work, then calls back to router.handleLocationChange() to let the router know it can proceed with transitioning to the new URL path.

The router then calls its own router.dispatch() method with the new URL, which handles the details of determining which of the configured routes match the URL, then calls any transition hooks present for the matched routes. You can implement these transition hooks on any of your route handlers to take some action when a route is about to be navigated away from or navigated to, with the ability to abort the transition if things aren't to your liking.

If the transition wasn't aborted, the final step is to call the callback you gave to Router.run() with the top-level handler component and a state object with all the details of the URL and the matched routes. The top-level handler component is actually the Router instance itself, which handles rendering the top-most route handler which was matched.

The above process is re-run every time you navigate to a new URL on the client.

Example projects

Jonny Buchanan
  • 61,926
  • 17
  • 143
  • 150
  • 3
    So I could probably say that the client routing is handled by javascript (which is react-router code) if it presents. Whenever I hit enter on the browser address bar or refresh the page or disable JS, the server side will handle the routing. On the other hand, when the javascript is ready on the current page, the routing will be handled by client side. Did I understand correctly ? – heartmon Feb 17 '15 at 11:06
  • 9
    What is in the routes module `var routes = require('./routes')` Is it a list of routes? I have used Express router but this example here on SO seems to be the only example of setting up server side rendering with React Router, so it would be good if it was a full code example – svnm Apr 29 '15 at 04:20
  • 2
    It should be a list of routes. I'll add a note about that and some links to example projects. – Jonny Buchanan Apr 29 '15 at 09:54
  • 2
    So if react-router takes care of server side routing then who does the talking to the database? what happens to server side routing? imagine we want to provide a REST API for a native mobile app. Who takes care of that? – Morteza Shahriari Nia May 16 '15 at 04:07
  • 1
    Answer is outdated it due to newer `react-router` version. Please update it. – oleh.meleshko Jan 13 '16 at 11:24
  • See here http://stackoverflow.com/questions/32682854/react-router-run-is-not-a-function re: new react-router version. – Oli Beatson Feb 03 '16 at 01:25
  • I use React and react-router 13.3. Why i'm receiving `var html = React.renderToString() SyntaxError: Unexpected token <` when attempting to run server? – stkvtflw Mar 22 '16 at 06:36
  • So I'm assuming that one could use `react-router` for certain sub-routes? For example, if someone wanted to use react to create a dashboard it would be possible to mount a `react-router` just to all `/dashboard` routes? – Kenny Worden Jun 17 '16 at 13:37
27

With 1.0, React-Router depends on the history module as a peerDependency. This module deals with routing in the browser. By default React-Router uses the HTML5 History API (pushState, replaceState), but you can configure it to use hash-based routing (see below)

The route handling is now done behind the scenes, and ReactRouter sends new props down to the Route handlers when the route changes. The Router has a new onUpdate prop callback whenever a route changes, useful for pageview tracking, or updating the <title>, for example.

Client (HTML5 routing)

import {Router} from 'react-router'
import routes from './routes'

var el = document.getElementById('root')

function track(){
  // ...
}

// routes can be children
render(<Router onUpdate={track}>{routes}</Router>, el)

Client (hash-based routing)

import {Router} from 'react-router'
import {createHashHistory} from 'history'
import routes from './routes'

var el = document.getElementById('root')

var history = createHashHistory()

// or routes can be a prop
render(<Router routes={routes} history={history}></Router>, el)

Server

On the server, we can use ReactRouter.match, this is taken from the server rendering guide

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

app.get('*', function(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) {
      res.status(200).send(renderToString(<RoutingContext {...renderProps} />))
    } else {
      res.status(404).send('Not found')
    }
  })
})
AnilRedshift
  • 7,937
  • 7
  • 35
  • 59
tom
  • 718
  • 8
  • 15