Configure BrowserRouter and Nested Routes in React Router v6
A practical React Router v6 guide to nested routes, client-side navigation, wildcard pages, and the server fallback required for direct URLs.
26 Jun 2026, 02:01 UTC

The problem: working links but broken direct URLs
A React application can navigate correctly after a button click and still fail when someone opens /projects/42 in a new tab or refreshes the page. The useful distinction is between client-side routing and server routing. BrowserRouter can change the address bar without loading a new document, but the server must still return the application's entry file for direct requests to application routes.
This guide builds a nested React Router v6 route tree, then verifies navigation, direct URLs, unmatched paths, and browser history. The result is a small application shell whose layout remains mounted while its child route changes.
Version and environment assumptions
The examples assume a client-rendered React application using the declarative APIs from react-router-dom v6.x. React Router v7 includes library and framework-oriented workflows, so confirm the installed version and import paths before copying the example into a v7 project.
From the project root, run:
npm ls react-router-dom react react-dom
This command only reads the local dependency tree and needs no elevated permissions. It should list the installed versions of the router and React packages. A nonzero exit caused by an unrelated peer dependency does not by itself prove that routing is broken; inspect the project's manifest and lockfile instead of upgrading packages during the routing change.
Choose the URL strategy before writing routes
BrowserRouter uses the browser's History API, which is the interface for changing the address bar without requesting a new HTML document. That produces clean URLs, but it also creates a server requirement.
| Decision | BrowserRouter | HashRouter |
|---|---|---|
| Example URL | /projects/42 | /#/projects/42 |
| Server behavior | Application routes need a fallback to the SPA entry file. | The fragment is not sent in the HTTP request, so a static host can usually serve the root file. |
| Main tradeoff | Clean URLs and normal-looking paths, with server configuration. | Less server work, but fragment-style URLs and different navigation semantics. |
| Reasonable choice when | The hosting environment supports route rewrites. | Rewrites are unavailable and fragment URLs are acceptable. |
Exclude API endpoints and other non-application paths from an SPA fallback. Otherwise, a request intended for an API may incorrectly receive the application's HTML file.
Build the route tree around a persistent layout
A Route associates a URL pattern with the element to display. A Routes component selects the best matching route branch. In v6, matching is ranked rather than controlled by source order: a static segment such as new outranks a dynamic segment such as :projectId.
Replace or adapt the application's route file with the following structure:
import {
BrowserRouter,
Link,
Outlet,
Route,
Routes,
useParams
} from 'react-router-dom';
function Layout() {
return (
<header>
<nav aria-label='Primary'>
<Link to='/'>Home</Link>
<Link to='/projects'>Projects</Link>
<Link to='/projects/42'>Project 42</Link>
</nav>
</header>
<main>
<Outlet />
</main>
);
}
function Home() {
return <h1>Home</h1>;
}
function Projects() {
return (
<>
<h1>Projects</h1>
<Outlet />
</>
);
}
function ProjectDetail() {
const { projectId } = useParams();
return <h1>Project {projectId}</h1>;
}
function NotFound() {
return <h1>Page not found</h1>;
}
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path='/' element={<Home />} />
<Route element={<Layout />}>
<Route path='projects' element={<Projects />}>
<Route path=':projectId' element={<ProjectDetail />} />
</Route>
<Route path='*' element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
);
}
The route without a path is a layout route. It renders Layout while its child routes change. Outlet is the placeholder where a parent route renders its matched child; without it, the child component has nowhere to appear. The :projectId segment is a dynamic parameter, and useParams exposes its value to ProjectDetail.
Keep BrowserRouter above every component that uses router hooks. A component calling useParams outside the router context will throw because no matching router context exists.
Use Link for internal navigation
Use Link for transitions within the application:
<Link to='/projects/42'>Open project 42</Link>
A normal <a href='/projects/42'> asks the browser to navigate to a new document. That reloads the application and resets in-memory React state, even though data stored on the server may remain intact. Keep ordinary anchors for external destinations, downloads, or deliberate full-document navigation.
The wildcard route at path='*' supplies a client-side fallback for unmatched paths. It controls the rendered interface, but it does not automatically make the server return an HTTP 404 status. If the status code matters, the server needs separate handling.
Run focused verification checks
Start the project's existing development script from the project root:
npm run dev
Use the local URL printed by the tool. This normally requires no elevated permissions, but it opens a local port; use the script's documented port option if that port is occupied. Do not expose the development server publicly unless that is intentional.
| Check | Expected result | Useful diagnostic |
|---|---|---|
| Click the project link | The URL becomes /projects/42, and the detail heading appears. | In browser developer tools, expect no new HTML document request. JavaScript or data requests may still occur. |
Open /projects/42 directly | The application loads and shows project 42. | If the server returns 404 before React starts, inspect its SPA fallback rather than the route component. |
Open /missing-page | The defined not-found component appears. | Confirm that the wildcard route remains inside the intended Routes tree. |
| Use browser back and forward | The previous matched route is restored. | Check that navigation used Link and that BrowserRouter wraps the route tree. |
| Inspect the rendered DOM | The layout and matched child are present; unmatched children are absent. | Nested routes legitimately render both parent and child, so check the matched branch rather than expecting one component. |
Recover from the two common failures
- If links work but direct URLs fail, correct the server's route fallback first. Changing the React component cannot repair a server response that never returns the application entry file.
- If a link causes a full reload, inspect the element for an ordinary anchor and replace it with
Linkwhen the destination is internal. - If the edit introduces an unrelated rendering regression, restore the changed route file from version control and reintroduce one route branch at a time. Avoid rolling back dependencies unless the version check identified an actual incompatibility.
What this setup does not cover
This route tree does not address server rendering, data loaders, framework-specific file routing, or authentication guards. It also assumes the hosting environment can distinguish application routes from API and asset requests. Confirm those behaviors in the environment that will serve the application, especially before relying on direct links or refreshes as a supported workflow.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.