React Routing works in local machine but not Heroku

I actually came across this post first before 3 hours of searching through react-router and heroku documentation. For swyx, and anyone else having the same problem, I'll outline the minimum of what you need to do to get this working.

router.js - (Obviously change AppSplash and AppDemo to your components)

export default <Router history={hashHistory}>
  <Route path="/" component={App}>
    <IndexRoute component={AppSplash}/>
    <Route path="demo" component={AppDemo}/>
  </Route>
</Router>

app.js

import React, { Component } from 'react'

class App extends Component {
static propTypes = {
  children: PropTypes.node
}

render() {
  const { children } = this.props
  return (
    <div>
      {children}
    </div>
  )
}
}

export default App

Create a new file in the root of your home directory and name it static.json. Put this into it.

{
  "root": "build/",
  "clean_urls": false,
  "routes": {
    "/**": "index.html"
  }
}

Push to heroku again. The routes should work this time.

Explanation:

You need to modify Heroku's default webpack, otherwise the service gets confused with how to handle the client-side routing. Essentially what static.json does. The rest is just the correct way to handle the routing according to the 'react-router' documentation.


How to fix client-side routing errors (Heroku 404 errors):

React Browser Router

If you're using React Browser Router, as an npm module with create-react-app, then the solution (which works for me) is to create a static.json file (within the same directory as package.json).

{
  "root": "build/",
  "clean_urls": false,
  "routes": {
    "/**": "index.html"
  }
}

Here is why this solution works:

Create-react-app is for the most part a Node.Js server which serves client-side React. The public static directory is mapped to the / endpoint, and visiting this endpoint from a browser will download the index.html webpage. This webpage in turn loads the React components. And because React Browser Router is a React component, the routes are loaded dynamically after visiting the / endpoint. In other words, before the index.html webpage is loaded all our React Browser Router routes will result in 404 errors on Heroku. To resolve this issue, a static.json file can be used to map any endpoints with the following pattern /** to the index.html file, which in turn will load React Browser Router and correctly load the react components for that route.

From an Apache HTTP server:

Likewise, on an Apache HTTP server creating an .htaccess file in the public directory, will remap all endpoints that match /** to the index.html file.

Options -MultiViews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.html [QSA,L]

More resources

Also read the "Deployment" section of the create-react-app README, which has a ton of good information on how to reconfigure the server to use client-side routing.

https://facebook.github.io/create-react-app/docs/deployment

React Static Router

Lastly, React Router offers a static router, React Static Router, which can be used with the "react-dom/server" npm module on a Node.js server, to render JSX server-side, and doesn't need static.json or .htaccess reconfiguration.


Try this:

app.get("*", (req, res) => {
  let url = path.join(__dirname, '../client/build', 'index.html');
  if (!url.startsWith('/app/')) // since we're on local windows
    url = url.substring(1);
  res.sendFile(url);
});

Worked for me when I put into server.js.


Webpack+Express Solution

TL;DR

Use res.sendFile but don't forget to also return transformed.js and other static files.

Explanations

I've been testing some of the answers on this thread, but none of them really worked for the following setup:

  • I am using webpack (-dev-server) on my local machine.
  • In production (on Heroku), I simply serve the webpack build output with a static express server. Something along the lines of app.use(express.static('${__dirname}/build'));.

This obviously does not work with a react router as static only returns actual files from the build folder (i.e. index.html) and will return 404's on any other url.

David Hahn's proposed solution to use res.sendFile pointed me to the right direction. However, the main issue with hijacking GET * is that the secondary request to transformed.js would also return index.html. After fixing the code to avoid this, I was able to get a working solution.

Code

Here's my server.js:

const express = require("express");

const port = process.env.PORT || 8080;
var app = express();

// List of all the files that should be served as-is
let protected = ['transformed.js', 'main.css', 'favicon.ico']

app.get("*", (req, res) => {

  let path = req.params['0'].substring(1)

  if (protected.includes(path)) {
    // Return the actual file
    res.sendFile(`${__dirname}/build/${path}`);
  } else {
    // Otherwise, redirect to /build/index.html
    res.sendFile(`${__dirname}/build/index.html`);
  }
});

app.listen(port, () => {
  console.log(`Server is up on port ${port}`);
});

Happy to discuss what you think! I am not a React veteran yet so there might be a better way. Cheers!