Skip to content

mhart/react-server-routing-example

Repository files navigation

react-server-routing-example

A simple (no compile) example of how to do universal server/browser rendering, routing and data fetching with React and AWS DynamoDB for fast page loads, and search-engine-friendly progressively-enhanced pages.

Also known as isomorphic, this approach shares as much browser and server code as possible and allows single-page apps to also render on the server. All React components, as well as router.js and db.js are shared (using browserify) and data fetching needs are declared statically on each component.

This example shows a very basic blog post viewer, Grumblr, with the posts stored in and fetched from DynamoDB whenever the route changes.

An even simpler example of server-side rendering with React, with no routing or data fetching, can be found at react-server-example.

Example

$ npm install
$ node server.js

Then navigate to http://localhost:3000 and click some links, press the back button, etc.

Try viewing the page source to ensure the HTML being sent from the server is already rendered (with checksums to determine whether client-side rendering is necessary).

Also note that when JavaScript is enabled, the single-page app will fetch the data via AJAX POSTs to DynamoDB directly, but when it's disabled the links will follow the hrefs and fetch the full page from the server each request.

Here are the files involved:

router.js:

// This is a very basic router, shared between the server (in server.js) and
// browser (in App.js), with each route defining the URL to be matched and the
// main component to be rendered

exports.routes = {
  list: {
    url: '/',
    component: require('./PostList'),
  },
  view: {
    url: /^\/posts\/(\d+)$/,
    component: require('./PostView'),
  },
}

// A basic routing resolution function to go through each route and see if the
// given URL matches. If so we return the route key and data-fetching function
// the route's component has declared (if any)
exports.resolve = function(url) {
  for (var key in exports.routes) {
    var route = exports.routes[key]
    var match = typeof route.url === 'string' ? url === route.url : url.match(route.url)

    if (match) {
      var params = Array.isArray(match) ? match.slice(1) : []
      return {
        key: key,
        fetchData: function(cb) {
          if (!route.component.fetchData) return cb()
          return route.component.fetchData.apply(null, params.concat(cb))
        },
      }
    }
  }
}

PostList.js:

var createReactClass = require('create-react-class')
var DOM = require('react-dom-factories')
var db = require('./db')
var div = DOM.div, h1 = DOM.h1, ul = DOM.ul, li = DOM.li, a = DOM.a

// This is the component we use for listing the posts on the homepage

module.exports = createReactClass({

  // Each component declares an asynchronous function to fetch its props.data
  statics: {
    fetchData: db.getAllPosts,
  },

  render: function() {

    return div(null,

      h1(null, 'Grumblr'),

      // props.data will be an array of posts
      ul({children: this.props.data.map(function(post) {

        // If the browser isn't JS-capable, then the links will work as per
        // usual, making requests to the server – otherwise they'll use the
        // client-side routing handler setup in the top-level App component
        return li(null, a({href: '/posts/' + post.id, onClick: this.props.onClick}, post.title))

      }.bind(this))})
    )
  },

})

PostView.js:

var createReactClass = require('create-react-class')
var DOM = require('react-dom-factories')
var db = require('./db')
var div = DOM.div, h1 = DOM.h1, p = DOM.p, a = DOM.a

// This is the component we use for viewing an individual post

module.exports = createReactClass({

  // Will be called with the params from the route URL (the post ID)
  statics: {
    fetchData: db.getPost,
  },

  render: function() {
    var post = this.props.data

    return div(null,

      h1(null, post.title),

      p(null, post.body),

      p(null, a({href: '/', onClick: this.props.onClick}, '< Grumblr Home'))
    )
  },

})

App.js:

var React = require('react')
var createReactClass = require('create-react-class')
var router = require('./router')

// This is the top-level component responsible for rendering the correct
// component (PostList/PostView) for the given route as well as handling any
// client-side routing needs (via window.history and window.onpopstate)

module.exports = createReactClass({

  // The props will be server-side rendered and passed in, so they'll be used
  // for the initial page load and render
  getInitialState: function() {
    return this.props
  },

  // When the component has been created in the browser, wire up
  // window.onpopstate to deal with URL updates
  componentDidMount: function() {
    window.onpopstate = this.updateUrl
  },

  // This click handler will be passed to all child components to attach to any
  // links so that all routing happens client-side after initial page load
  handleClick: function(e) {
    e.preventDefault()
    window.history.pushState(null, null, e.target.pathname)
    this.updateUrl()
  },

  // Whenever the url is updated in the browser, resolve the corresponding
  // route and call its data-fetching function, just as we do on the server
  // whenever a request comes in
  updateUrl: function() {
    var route = router.resolve(document.location.pathname)
    if (!route) return window.alert('Not Found')

    route.fetchData(function(err, data) {
      if (err) return window.alert(err)

      // This will trigger a re-render with (potentially) a new component and data
      this.setState({routeKey: route.key, data: data})

    }.bind(this))
  },

  // We look up the current route via its key, and then render its component
  // passing in the data we've fetched, and the click handler for routing
  render: function() {
    return React.createElement(router.routes[this.state.routeKey].component,
      {data: this.state.data, onClick: this.handleClick})
  },

})

browser.js:

var React = require('react')
var ReactDOM = require('react-dom')
var App = React.createFactory(require('./App'))

// This script will run in the browser and will render our component using the
// value from APP_PROPS that we generate inline in the page's html on the server.
// If these props match what is used in the server render, React will see that
// it doesn't need to generate any DOM and the page will load faster

ReactDOM.render(App(window.APP_PROPS), document.getElementById('content'))

server.js:

About

An example using universal client/server routing and data in React with AWS DynamoDB

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •