What you'll learn
Quick Answer
React Router swaps components based on the URL without reloading the page. Use Link rather than an anchor tag, or you lose all your state. On deployment, configure the server to serve index.html for every path or refreshing a route returns 404.
What client-side routing means
A traditional site asks the server for a new HTML document on every navigation. A single page app loads one document and then swaps what is displayed, updating the address bar to match.
The benefit is speed — no full reload, no white flash, and state such as a logged-in user or a filled form survives navigation. The cost is that you now own routing, and the browser's default behaviour has to be intercepted.
npm install react-router-dom
The basic setup
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
BrowserRouter wraps the app and watches the URL. Routes picks the first matching Route and renders its element. The * path is the catch-all — add it from the start, because without one an unknown URL renders nothing at all and looks like a broken app.
Link, not an anchor tag
This is the most common beginner mistake and its symptom is confusing.
<a href="/about">About</a> {/* wrong */}
<Link to="/about">About</Link> {/* right */}
An anchor tag triggers a full browser navigation. The page reloads, React restarts, and every piece of state is lost — the user is logged out of the UI, the form is cleared, the cart empties. It looks like it works, just slower and with mysterious state loss.
Link intercepts the click, updates the URL through the History API and re-renders. No reload, no state loss.
Use NavLink instead when you want to style the active item — it applies an active class automatically, which saves comparing the current path by hand.
Route parameters and programmatic navigation
<Route path="/user/:id" element={<UserPage />} />
import { useParams, useNavigate } from "react-router-dom";
function UserPage() {
const { id } = useParams();
return <h1>User {id}</h1>;
}
function LoginForm() {
const navigate = useNavigate();
async function onSubmit() {
await login();
navigate("/dashboard");
}
}
useParams reads the :id segment. Note it is always a string — comparing it to a numeric ID with === fails silently, which is a genuinely common bug.
useNavigate moves programmatically, for redirecting after login or after a form submits. For query strings such as ?page=2, use useSearchParams, which works much like React state.
The 404-on-refresh problem
Everything works locally. You deploy, click through to /about, refresh, and get a 404. The app is not broken — this is a server configuration issue and it catches almost everyone once.
When you click a Link, the server is never contacted; React handles it. When you refresh, the browser genuinely requests /about from the server, which looks for a file at that path, finds none, and returns 404.
The fix is to tell the server to serve index.html for any path it does not recognise, letting React Router take over once the app loads. On Netlify, a _redirects file containing /* /index.html 200. On Apache, a rewrite rule in .htaccess. On nginx, try_files $uri /index.html.
Most hosts that advertise single page app support do this automatically. See deploying your project for free for the wider set of first-deployment problems.
