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!