Skip to main content

Command Palette

Search for a command to run...

Why React and Vue SPAs Show 404 Errors After Deployment (And How to Fix Them)

Why client-side routing works on localhost but breaks on refresh in production

Updated
4 min read
Why React and Vue SPAs Show 404 Errors After Deployment (And How to Fix Them)
O
Hello! I'm Omteja Yallapragada, a final-year Computer Science student and Associate Product Engineer Intern at DeltaX. I build backend systems and APIs, with a strong focus on performance, scalability, and real-world problem solving. My experience with data structures and algorithms helps me reason about edge cases and system behavior effectively. I'm particularly interested in backend engineering and distributed systems, and I aim to build reliable, scalable software that makes a meaningful impact.

It works perfectly on localhost:3000. You click around, routes transition instantly, and everything is smooth.

But the moment you deploy to Vercel, Netlify, Nginx, or Firebase, disaster strikes:

  1. You share a direct link like yoursite.com/about ➡️ 404 Page Not Found

  2. You hit refresh while viewing /dashboard ➡️ 404 Page Not Found

Why does the production server "lose" pages that were working seconds ago? Let's break it down in under 3 minutes.


1. The Core Issue: Client-Side Routing is an Illusion

React (React Router) and Vue (Vue Router) are Single Page Applications (SPAs). There is physically only one HTML file on your hosting server: index.html.

When you navigate:

  • Clicking a Link: JavaScript intercepts the click, updates the URL bar locally using the browser's HTML5 History API (pushState), and swaps out the UI components. No request is sent to the server.

  • Hitting Refresh / Direct URL: The browser bypasses JavaScript and asks the hosting server: "Give me the file named /about."

Since there is no physical about.html file or about/ folder on the server, it serves a standard 404 error.

Conceptual Diagram: Client-Side Routing vs. Server-Side Routing Flow

2. Why Did It Work on Localhost?

Your local dev server (Vite, Webpack, or Create React App) runs a hidden middleware called SPA Fallback (connect-history-api-fallback). When you refresh /about, the dev server silently redirects the request back to index.html behind the scenes, allowing the JavaScript router to load and parse the path.

Production servers don't do this automatically.

Comparison: Localhost Fallback vs. Production Server Looking for Real Files

3. The Quick Fix: Rewrite Rules for All Platforms

To fix this, we must configure our production server to route all traffic back to index.html using a Rewrite (serving index.html content while preserving the requested URL so the JS router can parse it).

Config Infographic: How Rewrites Point Back to index.html

Vercel (vercel.json in root)

{
  "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}

Netlify (_redirects in build folder, e.g. public/)

/*    /index.html   200

Firebase Hosting (firebase.json in root)

{
  "hosting": {
    "rewrites": [{ "source": "**", "destination": "/index.html" }]
  }
}

Nginx (nginx.conf site configuration)

location / {
    try_files \(uri \)uri/ /index.html;
}

Apache (.htaccess in build folder)

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

GitHub Pages (No-Config Hack)

GitHub Pages is a static server with no rewrite support. Your options:

  1. Hash Mode: Use HashRouter or createWebHashHistory() which maps URLs to yoursite.com/#/about (the server only reads / and never sees the hash).

    URL Anatomy: How the Browser Splits Hash URLs
  2. The 404 Hack: Duplicate your index.html and name the copy 404.html. When GitHub Pages throws a 404, it loads your app, which reads the URL and renders the page correctly.


4. Alternative: Hash Mode vs. History Mode

If you don't have access to server configurations, you can switch your router to Hash Mode:

// React Router
// <HashRouter> instead of <BrowserRouter>

// Vue Router v4 (Vue 3)
// history: createWebHashHistory() instead of createWebHistory()

// Vue Router v3 (Vue 2)
// mode: 'hash' instead of mode: 'history'
  • Pros: Works out of the box anywhere; no server config needed.

  • Cons: Less clean URLs (/#/about); poor SEO as search engines generally ignore hash parameters.


💡 Troubleshooting Checklist

  • Placed config files (_redirects, .htaccess) in the public folder so Vite/Webpack copies them to dist.

  • Rebuilt your project (npm run build) and redeployed.

  • Confirmed you configured a Rewrite (200 Status) rather than a Redirect (which changes the URL).