ReactHosting.dev
← All guides

How to fix React Router 404 on refresh (Netlify, Vercel, Cloudflare & traditional servers)

By Andrius Vaitiekūnas

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 /api if a file named api.something exists
  • RewriteCond %{REQUEST_FILENAME} !-f — only rewrite if the requested path is not a real file, so your JS bundles, CSS, and images are still served directly
  • RewriteRule ^ index.html [QSA,L] — everything else goes to index.html. QSA preserves query strings, L stops 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:

  1. Open your homepage and click through to a nested route — should work as before
  2. Hit refresh on the nested route — should load, not 404
  3. Paste a deep link (e.g. yourdomain.com/pricing) into a new incognito tab — should load
  4. 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 RewriteCond line)

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.

AV

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.

About this site Contact

Related guides