Skip to content

React Tutorial Modern React and Production

Deploying a React App: Static Hosting and SPA Fallbacks

Lesson 134 of 156 4 min read By Deepak Paswan Updated Oct 10, 2026

Full-project lesson · needs Node.js, npm and real project files, so it does not run in the in-page playground.

A client-side React app is static files: HTML, JavaScript, CSS and images. Deploying it means putting the contents of dist on a web server. Two details decide whether it works: the base path and the fallback to index.html for app routes.

We tested the deployment advice in this lesson on a real Apache 2.4 server (the kind used by most shared hosting), with a React Router app, in three setups. The results are quoted below.

Step 1: build

terminal
BASH
npm run build

This creates dist. Upload the contents of dist (not the folder itself, unless you want the app inside a dist subfolder) to the web root, or to the subfolder where the app will live.

Step 2: tell the app where it lives

If the app is at the root of a domain (https://example.com/), the default base: '/' is right.

If it lives in a subfolder (https://example.com/shop/), set both:

  1. Vite’s base, so asset URLs are correct: npx vite build --base=/shop/ (or base: '/shop/' in vite.config.js).
  2. The router’s basename, so routes are matched correctly. With React Router, import.meta.env.BASE_URL already holds the base value, so you do not repeat it:
src/main.jsx
JSX
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <BrowserRouter basename={import.meta.env.BASE_URL}>
      <App />
    </BrowserRouter>
  </StrictMode>,
)
src/App.jsx
JSX
import { Routes, Route, Link } from 'react-router'

function App() {
  return (
    <div>
      <nav><Link to="/">Home</Link> | <Link to="/about">About</Link></nav>
      <Routes>
        <Route path="/" element={<h1>Home page</h1>} />
        <Route path="/about" element={<h1>About page</h1>} />
        <Route path="*" element={<h1>Page not found</h1>} />
      </Routes>
    </div>
  )
}

export default App

(Built at the root and checked in a browser. The subfolder results are below.)

With a client-side router, /about is not a file. If a visitor refreshes on /about, or opens a shared link to it, the server receives a request for /about and finds nothing. The server must answer every unknown path with index.html, and let React Router take over.

Here is what we measured on Apache with the app deployed in a subfolder, without any rewrite rules:

code
OUTPUT
/react-deploy-test/        -> 200
/react-deploy-test/about   -> 404

The home page loads, but a refresh on /about is a 404 error. After adding this .htaccess file next to index.html:

code
APACHE
<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /react-deploy-test/
  # Real files (JS, CSS, images) are served as they are; a missing one stays a real 404
  RewriteRule ^assets/ - [L]
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /react-deploy-test/index.html [L]
</IfModule>

a browser test gave: clicking About changed the address to /react-deploy-test/about and showed the About page; opening /react-deploy-test/about directly returned 200 and showed the About page; and a missing file such as /react-deploy-test/assets/missing.js correctly stayed a 404.

Change /react-deploy-test/ to your own subfolder (use / for the site root: RewriteBase / and RewriteRule . /index.html [L]). The rules say: if the request is not an existing file or folder, serve index.html. It requires mod_rewrite and permission for .htaccess overrides, which most shared hosts provide.

Configuration for other hosts

HostHow to add the fallback
Apache (including most shared hosting)The .htaccess above
Nginxlocation / { try_files $uri $uri/ /index.html; } (standard configuration; not tested in this course)
NetlifyA file public/_redirects containing /* /index.html 200
VercelA vercel.json with a rewrite of all paths to /index.html
Cloudflare Pages, GitHub Pages and othersCheck the host’s documentation for single-page-app mode; some need a 404.html copy of index.html

Files in public/ are copied into dist, so you can keep _redirects or .htaccess there and they are deployed automatically with every build.

What happens when the base is wrong

We also built the same app with the default base: '/' and uploaded it to a subfolder. The result was a blank page, with two 404 errors in the browser console: the page asked for /assets/index-....js at the domain root, where the file does not exist. Whenever a deployed page is blank, open the Network tab and check the URLs of the failed requests. See Common Production Errors.

Unknown URLs still return status 200

In the same test, /react-deploy-test/nonexistent-page showed the app’s “Page not found” screen, but the HTTP status was 200, not 404. That is a soft 404. For most apps behind a login it does not matter. If search engine indexing matters, see Client-Side React vs React Frameworks.

Caching

Because Vite puts a hash in every asset file name, assets can be cached for a year safely. index.html must not be cached for long, or users keep an old page that points at old files. A typical Apache setup (requires mod_expires, check that your host enables it):

code
APACHE
<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresByType text/html "access plus 0 seconds"
  ExpiresByType text/css "access plus 1 year"
  ExpiresByType application/javascript "access plus 1 year"
  ExpiresByType text/javascript "access plus 1 year"
  ExpiresByType image/svg+xml "access plus 1 year"
  ExpiresByType image/png "access plus 1 year"
</IfModule>

Also enable compression (mod_deflate for gzip, or Brotli if available). Compressed JavaScript is about a third of its size.

Deploying to shared hosting, step by step

  1. Decide the URL: root of a domain or a subfolder.
  2. Set base (and basename for the router) to match it.
  3. Run npm run build and test with npm run preview.
  4. Upload the contents of dist with the host’s file manager or FTP/SFTP (or Git deployment if offered).
  5. Add the .htaccess fallback.
  6. Visit the site, click through, refresh on an inner page, open the browser console and check for failed requests.
  7. Check that the site uses HTTPS.

Automating

Once it works by hand, automate: a CI job runs lint, tests and the build, and a deployment step uploads dist. Many hosts connect to Git and build for you. Keep .env values for each environment in the host’s settings, not in Git (Environment Configuration).

Key points

  • The build output is static: upload the contents of dist.
  • Set base and the router basename for subfolders, and add a fallback to index.html, or refresh gives a 404.
  • Cache hashed assets for a long time, never cache index.html, enable HTTPS and compression.