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

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:
You share a direct link like
yoursite.com/about➡️ 404 Page Not FoundYou 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.
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.
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).
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:
Hash Mode: Use
HashRouterorcreateWebHashHistory()which maps URLs toyoursite.com/#/about(the server only reads/and never sees the hash).
The 404 Hack: Duplicate your
index.htmland name the copy404.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 thepublicfolder so Vite/Webpack copies them todist.Rebuilt your project (
npm run build) and redeployed.Confirmed you configured a Rewrite (200 Status) rather than a Redirect (which changes the URL).



