Building and Deploying

Once a React app works on your machine, you need to turn it into a set of static files a browser can load fast, then put those files somewhere the public can reach. This is the build and deploy step: your build tool bundles and minifies your JavaScript, CSS, and assets into a small dist folder, and a hosting provider serves that folder to visitors. This lesson covers how the build process works, how to configure it, and how to deploy to the most common free and paid hosts.

Overview: What “Building” Actually Does

While developing, you run a dev server (Vite, or Create React App’s older webpack server) that compiles your JSX on the fly, keeps everything in memory, and enables features like Hot Module Replacement (HMR) so edits appear instantly. That dev server is never meant for production — it is not optimized for file size or load speed, and it usually is not even accessible outside your machine.

Running a build command instead produces a static, production-ready bundle. Concretely, the build tool:

  • Transpiles JSX and modern JavaScript into plain JavaScript that runs in target browsers, using Babel or esbuild/SWC under the hood.
  • Bundles your many source files and imported npm packages into a small number of output files, so the browser makes far fewer network requests.
  • Minifies the code — stripping whitespace, comments, and shortening variable names — to shrink file size.
  • Code-splits the bundle, often per route, so the browser downloads only the JavaScript needed for the first screen and fetches the rest lazily.
  • Hashes filenames (e.g. index-4f3a9c2.js) so browsers can cache assets aggressively and safely bust that cache the moment content changes.
  • Inlines environment variables that were prefixed for the client (e.g. VITE_API_URL) directly into the compiled code, since there is no server process to read process.env from at runtime.

The result is a folder of static HTML, CSS, and JS files — typically named dist (Vite) or build (Create React App) — that can be hosted on literally any static file server, a CDN, or object storage. React itself does nothing special at deploy time; once built, your app is just static assets plus JavaScript that mounts into a root DOM node, exactly as it did with createRoot in development.

Syntax: The Build Command

# Install dependencies
npm install

# Create an optimized production build
npm run build

# Preview the production build locally before deploying
npm run preview
Command What it does
npm run dev Starts the development server with HMR; not for production.
npm run build Produces an optimized static bundle in dist/ (Vite) or build/ (CRA).
npm run preview Serves the built dist/ folder locally so you can sanity-check it before deploying.

These scripts live in package.json and simply invoke the underlying build tool (Vite, in modern projects). You rarely call Vite directly — npm run build is the standard entry point across nearly every React project.

Examples

Example 1: A Minimal App Ready to Build

// src/App.jsx
function App() {
  return (
    <main>
      <h1>Hello, Production!</h1>
      <p>This app was built with Vite and deployed as static files.</p>
    </main>
  );
}

export default App;
// src/main.jsx
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./index.css";

createRoot(document.getElementById("root")).render(<App />);

Output: Running npm run build reads these two files, transpiles the JSX, and writes something like dist/index.html, dist/assets/index-a1b2c3.js, and dist/assets/index-d4e5f6.css. Opening dist/index.html through a static server shows the rendered heading and paragraph, with no visible difference from the dev version — only the files behind it changed.

This is the simplest possible build target: no routing, no environment variables, nothing dynamic. It demonstrates that the source files you already write throughout the course are exactly what gets compiled — there is no separate “production version” of your code to author.

Example 2: Using Environment Variables Safely at Build Time

// src/api.js
const API_URL = import.meta.env.VITE_API_URL;

export async function fetchUsers() {
  const response = await fetch(`${API_URL}/users`);
  if (!response.ok) {
    throw new Error("Failed to fetch users");
  }
  return response.json();
}
# .env.production
VITE_API_URL=https://api.example.com

Output: When you run npm run build, Vite reads .env.production and replaces every occurrence of import.meta.env.VITE_API_URL in the compiled output with the literal string "https://api.example.com". There is no runtime lookup — the value is baked into the JavaScript bundle.

This matters because a React app has no server-side process at runtime to read secret environment variables from; anything prefixed VITE_ ends up readable in the shipped JavaScript. Never put API keys or secrets meant to stay private into client-side env variables — only public configuration like a base API URL belongs here.

Example 3: A Multi-Route App Needing SPA Fallback Routing

// src/App.jsx
import { Routes, Route } from "react-router-dom";
import Home from "./pages/Home.jsx";
import About from "./pages/About.jsx";

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
    </Routes>
  );
}

export default App;
// vercel.json
{
  "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}

Output: Visiting / or /about directly (not just navigating via links inside the app) correctly renders the matching page instead of a 404, because the rewrite rule tells the host to always serve index.html and let React Router handle the path client-side.

This is the single most common deployment bug with React Router: a static host has no idea /about is a valid “page” — it only knows about files. Without a rewrite/redirect rule falling back to index.html, refreshing any route other than / returns a 404 from the host itself, before React ever loads.

How It Works Step by Step

  1. You run npm run build. Vite starts a production compile: it resolves your import graph starting at src/main.jsx, transpiles JSX/TSX via esbuild, and bundles everything (including npm dependencies) into a handful of files.
  2. Assets are hashed and optimized. Images, CSS, and JS chunks get content hashes in their filenames, minified code, and often gzip/brotli-friendly output, then everything is written into dist/.
  3. You push to a host or run a deploy command. The host (Vercel, Netlify, GitHub Pages, a VPS with Nginx, etc.) copies the contents of dist/ onto its static file servers/CDN edge nodes.
  4. A visitor requests your domain. The host’s CDN serves index.html and the referenced JS/CSS bundle directly from the nearest edge location — there is no Node.js server rendering anything at this point.
  5. The browser executes the bundle. The JS file calls createRoot(...).render(<App />) exactly as it did in development, mounting your component tree into the DOM. From here on, React’s normal render → reconcile → commit cycle takes over inside the user’s browser.
  6. Subsequent deploys replace the files. Because filenames are content-hashed, a new deploy simply uploads new hashed files and swaps which ones index.html references — old cached assets in visitors’ browsers are safely ignored since their URLs no longer appear anywhere.

Deploying to Common Hosts

Host Typical setup
Vercel Connect the Git repo; Vercel detects Vite/CRA automatically, runs npm run build, and serves dist/. Zero config for most apps.
Netlify Connect the repo, set build command npm run build and publish directory dist; add a _redirects file with /* /index.html 200 for SPA routing.
GitHub Pages Set base in vite.config.js to your repo name, build, then push the dist/ folder to a gh-pages branch (often via the gh-pages npm package).
Static server (Nginx/S3) Upload dist/ contents as-is; configure the server to fall back to index.html for unknown paths if using client-side routing.

Common Mistakes

Mistake 1: Deploying the source folder instead of the build output.

// Wrong: uploading the whole project, including src/, node_modules/, etc.
// Hosting the raw src/App.jsx does nothing — browsers cannot execute JSX or unresolved imports.

Browsers cannot parse JSX or resolve bare module specifiers like import App from "./App.jsx" without a bundler. You must deploy only the contents of dist/ (or build/), which contain already-compiled, browser-ready JavaScript.

Mistake 2: Forgetting an SPA fallback route, causing 404s on refresh.

// fragment-nocompile: a static host with NO fallback rule configured
// Visiting example.com/about directly returns the host's default 404 page
// because no physical file named /about exists in dist/.

Fix it by adding a rewrite/redirect rule specific to your host (a vercel.json rewrite, a Netlify _redirects file, or an Nginx try_files directive) that serves index.html for any unmatched path, letting React Router take over client-side.

Mistake 3: Reading a non-VITE_-prefixed environment variable expecting it to appear in the build.

// fragment-nocompile
const secret = import.meta.env.API_SECRET; // undefined in the built bundle

Vite only exposes environment variables to client code when they start with VITE_; anything else is intentionally stripped out to avoid leaking server secrets into the browser bundle. Rename it VITE_API_SECRET if it is meant to be public, or keep it server-side only if it is sensitive.

Best Practices

  • Always run npm run build followed by npm run preview locally at least once before deploying, to catch build-only errors that never surface in dev mode.
  • Never commit .env files containing real secrets; use your host’s dashboard to set production environment variables instead.
  • Set up an SPA fallback rule from day one on any project using client-side routing, even if you haven’t hit the 404-on-refresh bug yet.
  • Let your build tool code-split by route (e.g. via React.lazy) for larger apps, so first-load JavaScript stays small.
  • Automate deploys via CI (a GitHub Action, or your host’s Git integration) rather than manually uploading files, so every deploy is reproducible and tied to a commit.
  • Check bundle size after major dependency additions — a build tool’s output size report will flag when a new library bloats your JS bundle unexpectedly.

Practice Exercises

  • Take any small React app you’ve built in this course, run npm run build, then npm run preview, and inspect the generated dist/ folder’s file names — note the content hashes.
  • Add a VITE_APP_NAME variable to a .env file, read it with import.meta.env.VITE_APP_NAME in a component, and confirm after building that the value appears literally in the compiled JS bundle.
  • Deploy a small React Router app to Netlify or Vercel, then try refreshing a nested route directly in the browser. If it 404s, add the correct fallback rule for your host and confirm it now works.

Summary

  • npm run build compiles, bundles, minifies, and hashes your app into a static dist/ folder — that folder, not your source code, is what gets deployed.
  • There is no server-side React process in a typical static deployment; the browser mounts your app the same way it does in development, just from optimized files.
  • Client-side routing requires a host-level fallback rule to index.html, or direct navigation to non-root routes will 404.
  • Only environment variables prefixed VITE_ are inlined into the client bundle — never place real secrets there.
  • Vercel, Netlify, and GitHub Pages all support zero-to-low-config deployment of a Vite-built React app; the core steps (build, then serve dist/) are the same everywhere.