Firebase Hosting With React & Vite in 2023

Creating a starter React app, and hosting it on Firebase Hosting, is very simple. First you create the app using Vite’s React template. Then, you initialize Firebase Hosting in the project. Finally, you run it locally to develop, and deploy it to Firebase when ready for the world to see!

Create a React app named my-great-app:

$ npm create vite@latest my-great-app -- --template react
$ cd my-great-app
$ npm install
$ npm run build

Initialize Firebase Hosting:

$ firebase init hosting

  • select account
  • select “Use an existing project” (or create a new one if you prefer)
  • select your project
  • public directory should be dist
  • answer y to “Configure a single-page app)?
  • answer n to Set up automatic builds?
  • answer n to the question about overwriting dist/index.html

Edit firebase.json and add this line:

"predeploy": "npm run build",

right after this line:

"public": "dist",

and save the file.

Finally, run your app! To develop locally, run npm run dev, and to deploy to Firebase Hosting, run firebase deploy

React in a Legacy App

I am working on a relatively old legacy web app. It’s a very complex system, and was built upon jQuery and uses requirejs for module loading. I have been slowly updating it to use ES5+ features and React. In particular, working with React in this app has been a bit painful. This app, you see, doesn’t have a build environment, so using webpack and babel aren’t available, at least not in the standard way. With a great deal of effort, it certainly can be made to work with a build environment, but my intention is to spend my energy on cleaning up the code and, when that task is complete, bringing in a standard build system. Ah, that will be great.

But, at least for today, I want to add some React code. I’ve done it before in this app, but since I have no build system, I’ve used React.createElement(). If you’ve tried to build anything more than a trivial Component using React.createElement(), you know that it gets old, fast.

So I want to just JSX, naturally. But with no build system, I wasn’t sure how to proceed. There are plenty of examples on how to use babel standalone and mark your script tags as "text/babel" to have your JSX code converted to JS on the fly. But, a couple problems. First, I’m using requirejs, and it doesn’t want to mark only some script tags as "text/babel" (it can mark all or none). I’ve even gone so far as modifying requirejs to only mark certain script tags as "text/babel" but that leads to the next problem: babel standalone doesn’t seem to compile the scripts imported by requirejs. I’m sure there’s a way, but I gave up on that approach.

What I really wanted was something that would just compile JSX files into JS, so I could write complex React Components and not worry about it.

I found some examples for using browserify and babel to basically do what a build system would do, but it ends up creating massive JS files with all kinds of transpiled code. Clearly I was doing this wrong.

I finally found that I can use a basic babel transformFile() call with some relatively simple configuration that mainly just compiles the React JSX into React JS with React.createElement() calls, and really no other change to my code.

I wrote a “watcher” script for this purpose, and now just keep it running in my IDE while I work:

 const args = require('minimist')(process.argv.slice(2));
 const fs = require("fs/promises");
 const path = require("path");
 const chokidar = require("chokidar");
 const babel = require("@babel/core");
 const options = {
   ignored: /(^|[\/\\])\../
 };
 async function compile(src) {
   return new Promise((resolve, reject) => {
     const info = path.parse(src);
     const dest = path.join(info.dir, `${info.name}.js`);
     babel.transformFile(src, {}, async (error, result) => {
       if (error) { return reject(error) }
       await fs.writeFile(dest, result.code);
       console.log(`compiled ${src} -> ${dest}`);
       resolve();
     });
   });
 }
 async function remove(src) {
   const info = path.parse(src);
   const dest = path.join(info.dir, `${info.name}.js`);
   fs.unlink(dest)
     .then(() => {
       console.log(`removed ${dest}`);
     })
     .catch(error => {
       // ignore
     });
 }
 (async () => {
   chokidar.watch('**/*.jsx', options).on('all', async (event, path) => {
     if (event === "add" || event === "change") {
       await compile(path);
     }
     if (event === "unlink" && args.delete) {
       await remove(path);
     }
   });
 })(); 

The babel configuration file (babel.config.json) looks like this:

{
   "presets": [
     [
       "@babel/env",
       {
         "targets": {
           "edge": "17",
           "firefox": "60",
           "chrome": "67",
           "safari": "11.1"
         },
         "useBuiltIns": "usage",
         "corejs": "3.6.5"
       }
     ],
     "@babel/preset-react"
   ],
   "plugins": ["@babel/plugin-syntax-class-properties"]
 }

For packages, I’ve installed these in my package.json file:

@babel/cli
@babel/core
@babel/preset-env
@babel/preset-react
chokidar minimist