react server side rendering with client side routing

1.7k views Asked by At

An initial server rendering for my homepage route ( / ) works fine.

Also, subsequent client side navigation to ( /#/page2 ) works fine.

However, if I load /#/page2 directly from the address bar, the server rendered homepage loads in the browser first and then visibly transitions to /#/page2, which is not what I want. I want only /#/page2 to show up without first flashing the homepage.

What's happening is that node is serving up the homepage for the request to /, and then when the response hits the client, the client is running the route handler for /#/page2. Both are behaving correctly. But it's not what I want.

How do I avoid this behavior?

I think what I need is a way to for both the server and client to be aware of the different routes and both be able to handle them (isomorphically), however, the fragment part of the url is not known to the server.

Anyone else have this problem?

This problem isn't react specific. It is specific to SSR to a deep link.

My node router handles "/" as follows

router.get('/', function(req, res) {
  var React = require('react');
  var Router = require('react-router');
  var Routes = require("../app/clapi-routes.jsx");

  var router = Router.create({location: req.url, routes: Routes});
  router.run(function(Handler, state) {
    var html = React.renderToString(<Handler/>);
    return res.render('index.ejs', {html:html});
  })
});

index.ejs is just:

<html lang="en">
  <head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="/css/json-inspector.css"/>
  </head>
<body style="margin:0">
  <%- html %>
  <script src="/build/bundle.js"></script>
</body>
</html>
2

There are 2 answers

1
Madd0g On BEST ANSWER

Stop using hash powered navigation. Everything that comes after # is client-side only and useless for something like this. So /#/page2 needs to become /page2.

I'm not sure about react, but the same issue exists with other routing systems and there it's really easy to turn off the # in urls.

In angular's ui-router is done like this $locationProvider.html5Mode(true);

Your server-side would need to know to react to all the URLs your client-side knows, but that's how robustness is achieved - doesn't matter how navigation happens (client-side event or link click), both client and server can both handle the scenario end-to-end.

0
Joe Hanink On

I've found out that there are two steps:

First: Change the react router to use Router.HistoryLocation instead of the default (HashLocation). This makes your routes use html5 push state and changes your route paths from /#/page2 to /page2

// in app.jsx (client side routing)

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

Second: Ensure that your node page routes all return the same "index.ejs". Otherwise your routes, like /page2 will 404 on a full page refresh (or deeplink)

// in server.js (server side routing)

router.get('*', function(req, res) {
  Router.run(Routes, req.path, function(Handler) {
    var html = React.renderToString(<Handler/>);
    return res.render('index.ejs', {html: html});
  });
});

Additionally: If you are serving "public" static assets, declare that before your routes, and remove any public/index.html you might have so that your node router handles / requests with the server rendered content.