How to fix React Router 404 on refresh (Netlify, Vercel, Cloudflare & traditional servers)
It's one of the most common React deployment questions there is: the app deploys fine, navigation works, but the moment someone refreshes the page or opens a deep link — 404. Nothing is broken in your code. It's a server configuration issue, and the fix takes two minutes once you understand what's happening. This guide covers the fix for wherever you deploy: managed platforms like Netlify, Vercel, and Cloudflare Pages, and traditional servers running Apache, LiteSpeed, or Nginx.
Affiliate disclosure: Some links on this page are affiliate links to hosting providers we cover. If you sign up through them, I may earn a small commission at no extra cost to you. This never affects which hosts are covered or how they're ranked.
Why the 404 happens
A single-page app has exactly one HTML file. When a visitor navigates from / to /pricing, React Router changes the URL with the History API — no request ever reaches the server. Everything works.
But when someone refreshes on /pricing, or opens that link directly, the browser asks the server for a file at /pricing. There is no pricing.html in your dist/ folder — there's only index.html. The server finds nothing and returns 404.
The fix is always the same idea: tell the server "if the requested file doesn't exist, serve index.html instead and let React Router figure out the route." What changes is where you declare that rule — so find your host below. (Not sure which host fits your app in the first place? See our roundup of the best hosting for React apps.)
Managed platforms: one small file
On JAMstack platforms the fix is a single config file committed to your repo — no server access involved.
Netlify
On Netlify , create a file named _redirects in your public/ folder (so Vite copies it into the build output):
/* /index.html 200 The 200 makes it a rewrite, not a redirect — the URL stays as-is and React Router takes over.
Cloudflare Pages
Cloudflare Pages supports the same _redirects file with the same content, also placed in public/:
/* /index.html 200 Vercel
On Vercel , add a vercel.json in your project root:
{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] } Note: some frameworks (like Next.js, or Vite deployed through Vercel's framework presets) handle this automatically — this is the fix for a plain Vite SPA.
Traditional servers: rewrite rules
Apache / LiteSpeed shared hosting (.htaccess)
Most traditional shared hosting runs Apache or LiteSpeed, both of which read .htaccess files. Create a file named exactly .htaccess in your web root (often public_html):
Options -MultiViews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.html [QSA,L] Line by line:
Options -MultiViews— disables Apache's content negotiation, which can hijack routes like/apiif a file namedapi.somethingexistsRewriteCond %{REQUEST_FILENAME} !-f— only rewrite if the requested path is not a real file, so your JS bundles, CSS, and images are still served directlyRewriteRule ^ index.html [QSA,L]— everything else goes toindex.html.QSApreserves query strings,Lstops further rules
Can't see the file?
Files starting with a dot are hidden by default. In your host's file manager or FTP client, enable Show hidden files — otherwise you might create a duplicate or edit the wrong file.
VPS with Nginx (try_files)
Nginx doesn't read .htaccess files at all — uploading one does nothing. The equivalent is one line in your server block. Edit your site config (e.g. /etc/nginx/sites-available/myapp):
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
root /var/www/myapp/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
} try_files $uri $uri/ /index.html; tells Nginx: try the exact file, then a directory, then fall back to index.html. Then test and reload:
sudo nginx -t
sudo systemctl reload nginx If your app also has an API, make sure the location /api/ proxy block comes before catch-all behavior takes effect, or the rewrite will swallow your API routes too.
Apps served from a subdirectory
If your app lives at yourdomain.com/app/ instead of the root, two things change. First, set the base path in vite.config.js so asset URLs resolve correctly:
export default {
base: '/app/'
} And tell React Router about it:
<BrowserRouter basename="/app"> Second, on an Apache or LiteSpeed server, the .htaccess goes inside the subdirectory itself (e.g. public_html/app/) and rewrites to the subdirectory's index:
Options -MultiViews
RewriteEngine On
RewriteBase /app/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.html [QSA,L] Verify the fix
After deploying, run through this quick checklist:
- Open your homepage and click through to a nested route — should work as before
- Hit refresh on the nested route — should load, not 404
- Paste a deep link (e.g.
yourdomain.com/pricing) into a new incognito tab — should load - Open dev tools → Network tab and confirm your JS/CSS assets return 200, not HTML — if assets return HTML, your rewrite is too aggressive (check the
RewriteCondline)
Common mistakes
The .htaccess is named wrong
It must be exactly .htaccess — not htaccess.txt. Windows editors love adding extensions; check in your host's file manager, not your local folder.
The file is in the wrong folder
It goes next to index.html in your web root. If you uploaded your whole dist/ folder as a subfolder, everything — including the .htaccess — is one level too deep.
Using .htaccess on a server running Nginx
Nginx silently ignores it. Use try_files in the server block instead (see above).
The _redirects file didn't make it into the build
On Netlify and Cloudflare Pages, _redirects must end up in the build output — put it in Vite's public/ folder, not the project root, and confirm it appears in dist/ after a build.
Browser cache hiding the fix
If it still 404s after deploying, hard-refresh (Ctrl+Shift+R) or test in incognito — the 404 response may be cached.
Summary
SPA 404s on refresh aren't a bug in your app — the server just doesn't know that index.html should handle every route. On Netlify and Cloudflare Pages it's a one-line _redirects file; on Vercel it's a vercel.json rewrite; on Apache or LiteSpeed it's a four-line .htaccess; on Nginx it's one try_files line. Set it up once per project and you'll never see the refresh 404 again.
Frequently asked questions
Why does my React app work on localhost but 404 in production?
Your local dev server (Vite or webpack) automatically serves index.html for every route. A production web server does not — it looks for a real file matching the URL path, finds nothing, and returns 404. You need a rewrite rule that sends unknown paths back to index.html, and every host has its own way to declare one.
Where exactly does the .htaccess file go?
Directly inside your web root (often public_html on shared hosting), next to your index.html — not inside a subfolder and not in your project source. If your app lives in a subdirectory like public_html/app, put the .htaccess in that subdirectory and adjust the RewriteBase accordingly.
Does this affect SEO?
The rewrite itself returns your index.html with a 200 status for every path, so crawlers never see a 404. Keep in mind that a client-side SPA still renders content with JavaScript — if SEO is critical, consider pre-rendering or a framework with static generation like Astro or Next.js.
Do I need this fix if I use HashRouter?
No. HashRouter keeps the route after a # symbol (yourdomain.com/#/about), which browsers never send to the server, so no server configuration is needed. The trade-off is uglier URLs and slightly worse SEO, which is why BrowserRouter plus a rewrite rule is usually the better setup.
Written by Andrius Vaitiekūnas
Developer who deploys and hosts React apps. I write the deployment guides I wish existed — tested on real projects, hard parts included.
Related guides
Deploy React + Node.js on a VPS with Nginx
The full path to running React and Node on your own server: creating and locking down the box, Node, PM2 and Nginx, HTTPS with Let's Encrypt, and safe updates. Starts by asking whether you should run a VPS at all.
Best hosting for React apps (2026) — an honest comparison
There is no single best host for React — it depends on your app. An even-handed comparison of Vercel, Netlify, Cloudflare Pages, Railway, and Render, plus managed options for when you outgrow free tiers.