{"id":13422651,"url":"https://github.com/mhart/react-server-routing-example","last_synced_at":"2025-10-24T04:03:24.443Z","repository":{"id":28711778,"uuid":"32232489","full_name":"mhart/react-server-routing-example","owner":"mhart","description":"An example using universal client/server routing and data in React with AWS DynamoDB","archived":false,"fork":false,"pushed_at":"2020-09-04T15:16:20.000Z","size":174,"stargazers_count":299,"open_issues_count":0,"forks_count":46,"subscribers_count":10,"default_branch":"master","last_synced_at":"2025-03-30T08:11:09.280Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mhart.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-03-14T21:20:29.000Z","updated_at":"2024-11-04T15:27:27.000Z","dependencies_parsed_at":"2022-09-11T08:42:00.603Z","dependency_job_id":null,"html_url":"https://github.com/mhart/react-server-routing-example","commit_stats":null,"previous_names":[],"tags_count":30,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mhart%2Freact-server-routing-example","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mhart%2Freact-server-routing-example/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mhart%2Freact-server-routing-example/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mhart%2Freact-server-routing-example/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mhart","download_url":"https://codeload.github.com/mhart/react-server-routing-example/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247457799,"owners_count":20941906,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-30T23:00:49.436Z","updated_at":"2025-10-24T04:03:24.386Z","avatar_url":"https://github.com/mhart.png","language":"JavaScript","readme":"react-server-routing-example\n----------------------------\n\nA simple (no compile) example of how to do universal server/browser rendering,\nrouting and data fetching with [React](http://facebook.github.io/react/) and\n[AWS DynamoDB](http://aws.amazon.com/dynamodb/) for fast page loads, and\nsearch-engine-friendly progressively-enhanced pages.\n\nAlso known as isomorphic, this approach shares as much browser and server code\nas possible and allows single-page apps to also render on the server. All\nReact components, as well as `router.js` and `db.js` are shared (using\n[browserify](http://browserify.org/)) and data fetching needs are declared\nstatically on each component.\n\nThis example shows a *very* basic blog post viewer, Grumblr, with the posts\nstored in and fetched from DynamoDB whenever the route changes.\n\nAn even simpler example of server-side rendering with React, with no routing or\ndata fetching, can be found at\n[react-server-example](https://github.com/mhart/react-server-example).\n\nExample\n-------\n\n```sh\n$ npm install\n$ node server.js\n```\n\nThen navigate to [http://localhost:3000](http://localhost:3000) and click some\nlinks, press the back button, etc.\n\nTry viewing the page source to ensure the HTML being sent from the server is\nalready rendered (with checksums to determine whether client-side rendering is\nnecessary).\n\nAlso note that when JavaScript is enabled, the single-page app will fetch the\ndata via AJAX POSTs to DynamoDB directly, but when it's disabled the links will\nfollow the hrefs and fetch the full page from the server each request.\n\nHere are the files involved:\n\n`router.js`:\n```js\n// This is a very basic router, shared between the server (in server.js) and\n// browser (in App.js), with each route defining the URL to be matched and the\n// main component to be rendered\n\nexports.routes = {\n  list: {\n    url: '/',\n    component: require('./PostList'),\n  },\n  view: {\n    url: /^\\/posts\\/(\\d+)$/,\n    component: require('./PostView'),\n  },\n}\n\n// A basic routing resolution function to go through each route and see if the\n// given URL matches. If so we return the route key and data-fetching function\n// the route's component has declared (if any)\nexports.resolve = function(url) {\n  for (var key in exports.routes) {\n    var route = exports.routes[key]\n    var match = typeof route.url === 'string' ? url === route.url : url.match(route.url)\n\n    if (match) {\n      var params = Array.isArray(match) ? match.slice(1) : []\n      return {\n        key: key,\n        fetchData: function(cb) {\n          if (!route.component.fetchData) return cb()\n          return route.component.fetchData.apply(null, params.concat(cb))\n        },\n      }\n    }\n  }\n}\n```\n\n`PostList.js`:\n```js\nvar createReactClass = require('create-react-class')\nvar DOM = require('react-dom-factories')\nvar db = require('./db')\nvar div = DOM.div, h1 = DOM.h1, ul = DOM.ul, li = DOM.li, a = DOM.a\n\n// This is the component we use for listing the posts on the homepage\n\nmodule.exports = createReactClass({\n\n  // Each component declares an asynchronous function to fetch its props.data\n  statics: {\n    fetchData: db.getAllPosts,\n  },\n\n  render: function() {\n\n    return div(null,\n\n      h1(null, 'Grumblr'),\n\n      // props.data will be an array of posts\n      ul({children: this.props.data.map(function(post) {\n\n        // If the browser isn't JS-capable, then the links will work as per\n        // usual, making requests to the server – otherwise they'll use the\n        // client-side routing handler setup in the top-level App component\n        return li(null, a({href: '/posts/' + post.id, onClick: this.props.onClick}, post.title))\n\n      }.bind(this))})\n    )\n  },\n\n})\n```\n\n`PostView.js`:\n```js\nvar createReactClass = require('create-react-class')\nvar DOM = require('react-dom-factories')\nvar db = require('./db')\nvar div = DOM.div, h1 = DOM.h1, p = DOM.p, a = DOM.a\n\n// This is the component we use for viewing an individual post\n\nmodule.exports = createReactClass({\n\n  // Will be called with the params from the route URL (the post ID)\n  statics: {\n    fetchData: db.getPost,\n  },\n\n  render: function() {\n    var post = this.props.data\n\n    return div(null,\n\n      h1(null, post.title),\n\n      p(null, post.body),\n\n      p(null, a({href: '/', onClick: this.props.onClick}, '\u003c Grumblr Home'))\n    )\n  },\n\n})\n```\n\n`App.js`:\n```js\nvar React = require('react')\nvar createReactClass = require('create-react-class')\nvar router = require('./router')\n\n// This is the top-level component responsible for rendering the correct\n// component (PostList/PostView) for the given route as well as handling any\n// client-side routing needs (via window.history and window.onpopstate)\n\nmodule.exports = createReactClass({\n\n  // The props will be server-side rendered and passed in, so they'll be used\n  // for the initial page load and render\n  getInitialState: function() {\n    return this.props\n  },\n\n  // When the component has been created in the browser, wire up\n  // window.onpopstate to deal with URL updates\n  componentDidMount: function() {\n    window.onpopstate = this.updateUrl\n  },\n\n  // This click handler will be passed to all child components to attach to any\n  // links so that all routing happens client-side after initial page load\n  handleClick: function(e) {\n    e.preventDefault()\n    window.history.pushState(null, null, e.target.pathname)\n    this.updateUrl()\n  },\n\n  // Whenever the url is updated in the browser, resolve the corresponding\n  // route and call its data-fetching function, just as we do on the server\n  // whenever a request comes in\n  updateUrl: function() {\n    var route = router.resolve(document.location.pathname)\n    if (!route) return window.alert('Not Found')\n\n    route.fetchData(function(err, data) {\n      if (err) return window.alert(err)\n\n      // This will trigger a re-render with (potentially) a new component and data\n      this.setState({routeKey: route.key, data: data})\n\n    }.bind(this))\n  },\n\n  // We look up the current route via its key, and then render its component\n  // passing in the data we've fetched, and the click handler for routing\n  render: function() {\n    return React.createElement(router.routes[this.state.routeKey].component,\n      {data: this.state.data, onClick: this.handleClick})\n  },\n\n})\n```\n\n`browser.js`:\n```js\nvar React = require('react')\nvar ReactDOM = require('react-dom')\nvar App = React.createFactory(require('./App'))\n\n// This script will run in the browser and will render our component using the\n// value from APP_PROPS that we generate inline in the page's html on the server.\n// If these props match what is used in the server render, React will see that\n// it doesn't need to generate any DOM and the page will load faster\n\nReactDOM.render(App(window.APP_PROPS), document.getElementById('content'))\n```\n\n`server.js`:\n```js\nvar http = require('http')\nvar browserify = require('browserify')\nvar literalify = require('literalify')\nvar React = require('react')\nvar ReactDOMServer = require('react-dom/server')\nvar DOM = require('react-dom-factories')\nvar AWS = require('aws-sdk')\n// Our router, DB and React components are all shared by server and browser\n// thanks to browserify\nvar router = require('./router')\nvar db = require('./db')\nvar body = DOM.body, div = DOM.div, script = DOM.script\nvar App = React.createFactory(require('./App'))\n\n// A variable to store our JS, which we create when /bundle.js is first requested\nvar BUNDLE = null\n\n// Just create a plain old HTTP server that responds to our route endpoints\n// (and '/bundle.js')\nvar server = http.createServer(function(req, res) {\n\n  // See if we have any component routes matching the requested URL\n  var route = router.resolve(req.url)\n\n  if (route) {\n\n    res.setHeader('Content-Type', 'text/html; charset=utf8')\n\n    // We have a matching route, so call its data-fetching function to get the\n    // props/data we'll need to pass to the top-level component\n    route.fetchData(function(err, data) {\n\n      if (err) {\n        res.statusCode = err.message === 'NotFound' ? 404 : 500\n        return res.end(err.toString())\n      }\n\n      // Define the props for the top level React component – here we have the\n      // key to lookup the component we want to display for this route, as well\n      // as any data we've fetched\n      var props = {\n        routeKey: route.key,\n        data: data,\n      }\n\n      // Here we're using React to render the outer body, so we just use the\n      // simpler renderToStaticMarkup function, but you could use any templating\n      // language (or just a string) for the outer page template\n      var html = ReactDOMServer.renderToStaticMarkup(body(null,\n\n        // The actual server-side rendering of our component occurs here,\n        // passing in `props`. This div is the same one that the client will\n        // \"render\" into on the browser from browser.js\n        div({\n          id: 'content',\n          dangerouslySetInnerHTML: {__html: ReactDOMServer.renderToString(App(props))},\n        }),\n\n        // The props should match on the client and server, so we stringify them\n        // on the page to be available for access by the code run in browser.js\n        // You could use any var name here as long as it's unique\n        script({\n          dangerouslySetInnerHTML: {__html: 'var APP_PROPS = ' + safeStringify(props) + ';'},\n        }),\n\n        // We'll load React and AWS from a CDN - you don't have to do this,\n        // you can bundle them up or serve them locally if you like\n        script({src: 'https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js'}),\n        script({src: 'https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.production.min.js'}),\n        script({src: 'https://cdn.jsdelivr.net/npm/react-dom-factories@1.0.2/index.min.js'}),\n        script({src: 'https://cdn.jsdelivr.net/npm/create-react-class@15.6.3/create-react-class.min.js'}),\n        script({src: 'https://sdk.amazonaws.com/js/aws-sdk-2.653.0.min.js'}),\n\n        // Then the browser will fetch and run the browserified bundle consisting\n        // of browser.js and all its dependencies.\n        // We serve this from the endpoint a few lines down.\n        script({src: '/bundle.js'})\n      ))\n\n      // Return the page to the browser\n      res.end(html)\n    })\n\n  // This endpoint is hit when the browser is requesting bundle.js from the page above\n  } else if (req.url === '/bundle.js') {\n\n    res.setHeader('Content-Type', 'text/javascript')\n\n    // If we've already bundled, send the cached result\n    if (BUNDLE != null) {\n      return res.end(BUNDLE)\n    }\n\n    // Here we invoke browserify to package up browser.js and everything it requires.\n    // We also use literalify to transform our `require` statements for React\n    // and AWS so that it uses the global variable (from the CDN JS file)\n    // instead of bundling it up with everything else\n    return browserify()\n      .add('./browser.js')\n      .transform(literalify.configure({\n        'react': 'window.React',\n        'react-dom': 'window.ReactDOM',\n        'react-dom-factories': 'window.ReactDOMFactories',\n        'create-react-class': 'window.createReactClass',\n        'aws-sdk': 'window.AWS',\n      }))\n      .bundle(function(err, buf) {\n        // Now we can cache the result and serve this up each time\n        BUNDLE = buf\n        res.statusCode = err ? 500 : 200\n        res.end(err ? err.message : BUNDLE)\n      })\n\n  // Return 404 for all other requests\n  } else {\n    res.statusCode = 404\n    return res.end('Not Found')\n  }\n\n})\n\n// We start the http server after we check if the DB has been setup correctly\nensureTableExists(function(err) {\n  if (err) throw err\n  server.listen(3000, function(err) {\n    if (err) throw err\n    console.log('Listening on 3000...')\n  })\n})\n\n\n// A utility function to safely escape JSON for embedding in a \u003cscript\u003e tag\nfunction safeStringify(obj) {\n  return JSON.stringify(obj)\n    .replace(/\u003c\\/(script)/ig, '\u003c\\\\/$1')\n    .replace(/\u003c!--/g, '\u003c\\\\!--')\n    .replace(/\\u2028/g, '\\\\u2028') // Only necessary if interpreting as JS, which we do\n    .replace(/\\u2029/g, '\\\\u2029') // Ditto\n}\n\n\n// A bootstrapping function to create and populate our DB table if it doesn't\n// exist (and start the mock DB if running locally)\nfunction ensureTableExists(cb) {\n  // Excluded for brevity...\n}\n```\n\n`db.js`:\n```js\nvar AWS = require('aws-sdk')\n\n// Because the AWS SDK works in the browser as well, we can share this file and all its\n// functions and reuse them on both the server and the browser\n\nvar db = module.exports = new AWS.DynamoDB({\n  // This endpoint will try to connect to a DynamoDB running locally\n  // Comment this out if you want to connect to a live/production AWS DynamoDB instance\n  endpoint: 'http://localhost:4567',\n  region: 'us-east-1',\n  // These credentials are only necessary if connecting to AWS,\n  // but including credentials in your client-side code is obviously\n  // problematic if your project is public facing.\n  // See http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/browser-configuring.html#Loading_Credentials_in_the_Client_s_Browser\n  // for other (safer) methods to authenticate users for the AWS SDK\n  credentials: {accessKeyId: 'akid', secretAccessKey: 'secret'},\n})\n\n// This function will fetch the id, date and title of all posts in our\n// \"grumblr\" table\ndb.getAllPosts = function(cb) {\n  db.scan({TableName: 'grumblr', AttributesToGet: ['id', 'date', 'title']}, function(err, res) {\n    if (err) return cb(err)\n    cb(null, res.Items.map(fromDynamo).sort(function(post1, post2) {\n      return post1.date.localeCompare(post2.date)\n    }))\n  })\n}\n\n// This function will fetch the detail of a particular posts from our \"grumblr\"\n// table\ndb.getPost = function(id, cb) {\n  db.getItem({TableName: 'grumblr', Key: {id: {S: id}}}, function(err, res) {\n    if (err) return cb(err)\n    if (!res.Item) return cb(new Error('NotFound'))\n    cb(null, fromDynamo(res.Item))\n  })\n}\n\n// A simple utility function to flatten a DynamoDB object\nfunction fromDynamo(dynamoObj) {\n  return Object.keys(dynamoObj).reduce(function(obj, key) {\n    obj[key] = dynamoObj[key][Object.keys(dynamoObj[key])[0]]\n    return obj\n  }, {})\n}\n```\n","funding_links":[],"categories":["Cloud Solutions","Open Source Repos"],"sub_categories":["Databases","Miscellaneous Repos"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmhart%2Freact-server-routing-example","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmhart%2Freact-server-routing-example","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmhart%2Freact-server-routing-example/lists"}