Search code examples
node.jsreactjsherokudeploymentstatic-site

How do I deploy a static React "app" to Heroku?


Title was: How do I fix "Cannot use import statement outside a module" error on Heroku without causing a "Must use import to load ES Module" error?

This was before I understood that I was trying to deploy a "static" React "app" and Heroku was running index.js as if it was server code.

I had setup Procfile to contain:

web: node ./src/index.js

Then I saw this error:

...
2020-03-29T02:34:01.000000+00:00 app[api]: Build succeeded
2020-03-29T02:34:02.637867+00:00 heroku[web.1]: State changed from starting to crashed
2020-03-29T02:34:02.622526+00:00 heroku[web.1]: Process exited with status 1
2020-03-29T02:34:02.583667+00:00 app[web.1]: /app/src/index.js:1
2020-03-29T02:34:02.583689+00:00 app[web.1]: import React from 'react';
2020-03-29T02:34:02.583690+00:00 app[web.1]: ^^^^^^
2020-03-29T02:34:02.583690+00:00 app[web.1]:
2020-03-29T02:34:02.583691+00:00 app[web.1]: SyntaxError: Cannot use import statement outside a module
2020-03-29T02:34:02.583691+00:00 app[web.1]: at wrapSafe (internal/modules/cjs/loader.js:1072:16)
...

I tried to fix it by adding this to package.json:

  "type": "module",

And then I got this error:

...
2020-03-29T03:05:01.000000+00:00 app[api]: Build succeeded
2020-03-29T03:05:06.293573+00:00 heroku[web.1]: Starting process with command `node ./src/index.js`
2020-03-29T03:05:08.744086+00:00 heroku[web.1]: State changed from starting to crashed
2020-03-29T03:05:08.724957+00:00 heroku[web.1]: Process exited with status 1
2020-03-29T03:05:08.650103+00:00 app[web.1]: internal/modules/cjs/loader.js:1174
2020-03-29T03:05:08.650125+00:00 app[web.1]: throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath);
2020-03-29T03:05:08.650126+00:00 app[web.1]: ^
2020-03-29T03:05:08.650126+00:00 app[web.1]:
2020-03-29T03:05:08.650126+00:00 app[web.1]: Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /app/src/index.js
2020-03-29T03:05:08.650127+00:00 app[web.1]: at Object.Module._extensions..js (internal/modules/cjs/loader.js:1174:13)
2020-03-29T03:05:08.650127+00:00 app[web.1]: at Module.load (internal/modules/cjs/loader.js:1002:32)
...

I should've noted that my React "app" is a front-end only that uses a GraphQL API I've already deployed elsewhere. In other words, there is no "server.js that serves [my] react app". (HMR's comment led me to that realization. I guess the correct terminology would be to call this a "static app".)

This code in server.js only slightly adapted from Tin Nguyen's answer works:

const http = require('http');
const path = require('path');
const express = require('express');

let wss;
let server;
const app = express();
app.use(express.static(path.join(__dirname, './build')));

server = new http.createServer(app);

server.on('error', err => console.log('Server error:', err));
server.listen(process.env.PORT);

...with this Procfile:

web: node ./server.js

The only change is the path to the build directory, which I tweaked after snooping around with heroku run bash.

My site isn't working properly, but I think that is the result of my build not being correct. Perhaps that can be solved with a buildpack. But that belongs in a different question. (And I might not bother figuring it out since I plan to take Tin Nguyen's advice and try hosting on GitHub.)

Update: The problem wasn't with my build, but with client-side routing (as described on https://create-react-app.dev/docs/deployment/). This code (from that page) fixes it (in server.js):

const express = require('express');
const path = require('path');
const app = express();

app.use(express.static(path.join(__dirname, 'build')));

app.get('/*', function (req, res) {
  res.sendFile(path.join(__dirname, 'build', 'index.html'));
});

app.listen(process.env.PORT);

The link in HMR's comment helped me reach this understanding, but Dave Ceddia writes that his article covers how to keep a React app and API server together. Is there any documentation on how to deploy a front-end on Heroku (or elsewhere) with an API server already deployed elsewhere?

(I found this answer where Paras recommends Netlify.)


Solution

  • If I am reading it right you have a static website and you want it to be hosted on Heroku.

    Static websites can be served on Netlify and also GitHub pages. They don't require a web server. You can still host it on Heroku by wrapping a web server around it that is then serving your static files.

    const http = require('http');
    const path = require('path');
    const express = require('express');
    
    let wss;
    let server;
    const app = express();
    app.use(express.static(path.join(__dirname, './../build')));
    
    server = new http.createServer(app);
    
    server.on('error', err => console.log('Server error:', err));
    server.listen(process.env.PORT);
    

    How do you know if you have a static website?
    You should have generated build files somewhere in your project. You can navigate to the folder and open index.html. The website should work even though you don't have node or npm running. That folder is in my example ./../build.

    Personally I would recommend you host static websites on GitHub over Heroku. Heroku is "free" but with a lot of limitations. The dyno hours are limited, websites need to spin up after not being in use for a long time, etc.