How to deploy a React app without a server, easy, no compromises

Written by
  • Mladen
    Name
    Mladen
    Title
    Software Engineer
Published on

The story

You have an idea of an awesome app you want to create and you have a good knowledge of React, so what do you do?

npx create-react-app myApp

cd myApp

npm start

and you are good to go! Everything is going really smoothly in your local environment and nothing stands in your way to creating that beautiful UI, leveraging the power of React all along the way.

Time passes by, you finish your app, and surely, you want to show your app to the world.

Maybe you are not ready for a full-fledged production-ready app with a domain, and all the bells and whistles, but only want to show it to someone else. You want to have it online without too much hassle.

So, what are your options?

When running npm run build to create an optimized production build of your app that create-react-app provides, info in the terminal window tells you this

The build folder is ready to be deployed.
You may serve it with a static server:

  npm install -g serve
  serve -s build

This means you need to have a server in place to get your application up and running.

The problem with servers is that computing power costs money, and there is a small chance you will find it unlimited for free anywhere, and also, you really aren't ready to pay money for your app at the moment. Other than that, setting up an HTTP server, getting a domain, etc. is a challenge on its own...

But, you are a seasoned React veteran. You know that building a React app gives you a folder with all the files you need to have an app working.

Cloud services

That is where the first major part of this puzzle gets solved... cloud computing!

But it's really not that exotic because I'm mostly talking about storage solutions those provide, Azure Blob Storage, and Amazon s3 buckets.

Both of them have an option to host a static website, and setting it up is fairly easy since both Azure and AWS provide free tier services for storage solutions that you cannot go out of if they are being used in a non-high load/traffic use-case. Definitely read the pricing tables for both of them if you plan to host something serious on them!

create-react-app on AWS s3

I will now show you a really basic create-react-app deployed to an s3 bucket, and later on, I will show you how to do it by yourself on Azure! You can easily find similar tutorials for deploying static websites on the AWS s3 bucket. Comment below if you want an AWS s3 deployment tutorial as well.

my really complex React app

You will immediately recognize the default create-react-app screen with a twist. I added a router and an "About" page with links that can guide you back and forth.

Our demo app works great until you try to refresh, or land directly on a page which is not our home page (e.g. /about). This is where any relatively serious app fails using this approach.

The problem is that a React app is a single-page app with just one index.html which executes js files who then do their magic and fill up our app with all the beautiful content.

If you take a look at the bucket for this app, you will quickly realize that there is no "about" folder with an index.html file inside of it, so we rightfully get a 404 error. We would need to have a server that redirects all of our traffic to this index.html and the javascript inside which will boot up our React app and figure out what we are trying to see.

Next.js

This is where Next.js comes in and saves the day!

If you don't know what Next.js is, seriously go look it up and do a bit of research, it's awesome!

I cannot give Next.js enough justice by doing this, but I will try to sum it up for anyone who hasn't heard about it before.

Next.js is a framework for React which mainly provides server-side rendering out of the box, and it can be seen only as an "extension" to React because you still only write regular js and jsx (ts/tsx also supported!), but it is much, much more! Next.js gives us a router out of the box and it only "forces" us to use the file system as routes, so every file inside the pages folder is a regular React component, but it is also a route.

For example, creating a component inside the pages folder like pages/about.js instantly registers the /about route to go to this file.

Next.js also provides us with some additional functions that help server-side data fetching which will come in handy pretty soon.

You can start a new Next.js project as easy as create-react-app.

npx create-next-app
# or
yarn create next-app

The project

I created a small Next.js app which I connected to a free cocktails API, fetched a couple of cocktails, listed them, created a detail page of every one of them. Here is the link to the project so you can check it out

https://staticappdemo.z6.web.core.windows.net/

I also provided the source code on github if you want to follow along.

I will try to explain how this is done pointing out a couple of "gotchas" inside Next.js and then do the step by step deployment to Azure!

You will notice that inside my pages folder I have a structure like this

+----_app.js
+----index.js
+----cocktails
|   +-- [id].js
|   +-- index.js

pages/cocktails/index.js is my /cocktails route, and pages/cocktails/[id].js is Next.js way to handle dynamic routes, so /cocktails/123abc will go to that file and we will have 123abc available to us inside this file as id.

Since Next.js provides us with static HTML export, we will use that feature to fetch our cocktails in build time and deploy everything as static pages to our cloud storage solution.

The first thing we need to do is use the getStaticPaths function that Next.js provides for us so that we can tell it what routes do we need to be generated at build time.

Inside our [id].js file you can see this piece of code.

// This function gets called at build time
export async function getStaticPaths() {
  // Call an external API endpoint to get cocktails
  const res = await fetch(
    "https://www.thecocktaildb.com/api/json/v1/1/filter.php?a=Alcoholic"
  );
  const parsedRes = await res.json();
  const cocktails = parsedRes.drinks.slice(0, 9);

  // Get the paths we want to pre-render based on cocktails
  const paths = cocktails.map((cocktail) => ({
    params: { id: cocktail.idDrink },
  }));

  // We'll pre-render only these paths at build time.
  // { fallback: false } means other routes should 404.
  return { paths, fallback: false };
}

As you can see here, we only fetch the cocktails for their ids and map them as per documentation so that Next.js knows those are the ids we want to have for our cocktail routes.

After that, you can see `getStaticProps` being used like this

// This also gets called at build time
export async function getStaticProps({ params }) {
  // params contains the cocktail `id`.
  // If the route is like /cocktails/1, then params.id is 1
  const res = await fetch(
    `https://www.thecocktaildb.com/api/json/v1/1/lookup.php?i=${params.id}`
  );
  const cocktail = await res.json();

  // Pass cocktail data to the page via props
  return { props: { cocktail: cocktail.drinks[0] } };
}

We use the id to fetch details of an individual cocktail and then pass it down for us to use it inside props.

Next.js doesn't know that we want to use it as a static HTML export so it won't create the file structure as we want it for that use case. We can quickly fix that by adding this piece of code inside next.config.js

module.exports = {
  trailingSlash: process.env.NEXT_PUBLIC_ENV === "prod",
};

This tells us to use the trailingSlash when doing a production build. You now need .env.development and .env.production files which Next.js will automatically recognize when building the app for production, or for you to use in your local environment.

To build the app as static HTML, I added a new script to `package.json`

"build:static": "next build && next export"

Running npm run build:static creates an out folder with all our pages built inside their own id folders. If everything went well, your out folder should look something like this:

Deploy to Azure

Creating a free Azure account should be pretty easy, and in the Azure dashboard, use the search bar on top to find the `Storage accounts` service. After entering `Storage accounts` you should see something like this

Click on Create storage account button or Add button on top left.

You will need to create a new resource group (if you haven't did that previously) and you can do that inside the wizard easily. It looks like this

Fill in the storage account name and choose a location geographically closest to you (or your audience).

Leave everything else as default and go straight for the Review + create button.

The deployment will take about a minute or less and you should now see your new storage account inside the Storage accounts dashboard

Click on the newly created storage account. It will open up a menu. Scroll down and find the Static website option. Enable it and fill the Index document name with index.html and Error document path with 404/index.html. Next.js provides default 404 page for us. Click the save button and you will have your new website endpoint ready! It should look something like this

Scroll the menu back up to Storage explorer (preview) click on it and open the BLOB CONTAINERS folder and there you will see a $web folder. That is where your built app files will be.

Before we can upload our files here, we need to add ourselves as a blob owner, otherwise, the upload will fail. To do that, on the menu, find Access Control (IAM) option. Click on the Add role assignments. Select the role Storage Blob Data Owner. Assign access to User, group, or service principal and in select field type in your e mail address associated with your Azure account. It should look like this

The next thing you want to do is install the Azure CLI for your OS

After that is done, enter your terminal and start with logging in to Azure

az login

A new browser tab should open up for you to log in to your Azure account.

After that, you need to find out the key for your storage account. To do that run

az storage account keys list --account-name 'mladenteststorage'

just replace mladenteststorage with your storage account name.

You should get an output that looks something like this:

az storage account keys list --account-name 'mladenteststorage'
[
  {
    "keyName": "key1",
    "permissions": "Full",
    "value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  },
  {
    "keyName": "key2",
    "permissions": "Full",
    "value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  }
]

Take the "value" from the "key1" and write it down.

And finally to upload everything to our blob storage

az storage blob upload-batch -d '$web' -s 'C:\Users\mlst2502\...path-to-your-project\out' --account-name 'mladenteststorage' --account-key 'key-that-you-wrote-down-previously'

And there you have it! Your app should be visible on the URL you saw in the `Static website` section of your storage account!

If you read this all the way till the end, thank you for your attention! I hope this helped you in any way!