Learn React Router in an Existing Project
Adding React Router to an existing project feels a little like renovating a house while still living in it. The lights are on, the furniture is already in place, and your users are walking through the front door every day. You cannot just tear everything down and start over. You need to introduce routing carefully, keep the app stable, and make the transition feel natural.
That is exactly why this topic matters so much.
A lot of tutorials show React Router in a brand-new app. That is useful, but the real world is different. Most of the time, you are not starting from zero. You already have pages, components, API calls, layouts, forms, and maybe even a small mess of conditional rendering that has been growing for months. Then one day you realize the app needs real navigation: a dashboard, profile pages, product pages, order details, admin panels, settings, and maybe a public site on top of that.
At that point, React Router becomes more than a library. It becomes the structure that helps your app feel like an actual application instead of one giant screen.
This guide walks through how to add React Router to an existing React project in a clean, practical way. We will build step by step, using realistic examples, and we will talk about common mistakes along the way. The goal is not just to “make routes work.” The goal is to make the routing feel like it always belonged there.
Why React Router matters in an existing project
In a small app, you can sometimes fake navigation with state. A button changes a boolean, a new component appears, and that is enough for a while. But as the app grows, that approach starts to crack.
A route gives you something state alone cannot:
A shareable URL
Browser back and forward support
Direct access to nested pages
Better organization of large screens
Cleaner separation between sections of the app
If someone visits /dashboard/orders/42, they should land directly on that order. If they refresh the browser, the page should still load correctly. If they copy the URL and send it to a teammate, that teammate should arrive at the same place. Routing makes that possible.
For an existing project, this is often the moment where the app becomes easier to maintain too. Instead of one large component trying to manage everything, you get a route-based structure where each page has a clear responsibility.
Before you start: understand your current project
Before adding React Router, take a quiet look at your app as it already exists.
Ask yourself:
Does the app already have a layout component?
Is there a navbar or sidebar that should stay visible on every page?
Are there any pages that should be public and others that should be private?
Do you have components that are really “pages” but are not treated like pages yet?
Are you using React Router already in a limited way, or are you starting from scratch?
This matters because existing projects rarely need a full rewrite. They usually need a careful reorganization.
For example, if your app already has a top navigation bar and a left sidebar, those should probably become part of a shared layout route. The content inside the page should change, but the shell around it should stay consistent.
That shared shell is one of the biggest strengths of React Router.
Installing React Router
If your project does not already include it, install react-router-dom.
npm install react-router-dom
Or with Yarn:
yarn add react-router-dom
Or with pnpm:
pnpm add react-router-dom
If you are working in an existing codebase, this is usually the easiest part. The more important part comes next: placing the router in the right place in your app.
Wrap your app with BrowserRouter
React Router needs to know about the URL, and that means your application must be wrapped in a router provider.
In most React apps, this happens in main.jsx, main.tsx, index.jsx, or index.tsx.
Example with main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
)
That single change is the foundation. Without it, your routes and links will not work properly.
If your project already has providers for Redux, Context API, React Query, or theme handling, you may need to place BrowserRouter in the right position among them. Usually, it is fine to wrap App with it at the top level.
The simplest route setup
Now let us create a basic route structure.
App.jsx
import { Routes, Route } from 'react-router-dom'
import Home from './pages/Home'
import About from './pages/About'
import NotFound from './pages/NotFound'
export default function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>
)
}
This is the simplest working setup.
Now each URL renders a different component:
/rendersHome/aboutrendersAboutanything else renders
NotFound
That is enough to get started, but in a real project, this is only the beginning.
Organize your project before it becomes messy
If your existing project has grown organically, you may already feel the pain of components living in the wrong places. Routing is a good excuse to clean up the structure.
A common structure looks like this:
src/
components/
layouts/
pages/
routes/
hooks/
context/
assets/
A practical breakdown:
pages/for route-level screenslayouts/for shared page shellscomponents/for reusable UI piecesroutes/for route configuration if the app gets large
This is not a strict rule. It is simply a way to make the project easier to reason about. In an existing app, even a small cleanup like moving page-like components into pages/ can reduce confusion later.
A good sign that a component belongs in pages/ is this: if it is tied to a URL, it is probably a page.
Add shared layouts the right way
One of the most important features in React Router is the ability to create layout routes.
A layout route lets you keep common UI around multiple pages. Think of a dashboard with a sidebar, top bar, and footer. Only the main content area changes.
DashboardLayout.jsx
import { Outlet, NavLink } from 'react-router-dom'
export default function DashboardLayout() {
return (
<div style={{ display: 'flex', minHeight: '100vh' }}>
<aside style={{ width: 240, padding: 20, background: '#f4f4f4' }}>
<h2>My App</h2>
<nav style={{ display: 'grid', gap: 12, marginTop: 20 }}>
<NavLink to="/dashboard" end>
Dashboard
</NavLink>
<NavLink to="/dashboard/orders">Orders</NavLink>
<NavLink to="/dashboard/settings">Settings</NavLink>
</nav>
</aside>
<main style={{ flex: 1, padding: 24 }}>
<Outlet />
</main>
</div>
)
}
The important part is <Outlet />. That is where nested route content appears.
Route setup
import { Routes, Route } from 'react-router-dom'
import Home from './pages/Home'
import About from './pages/About'
import Dashboard from './pages/Dashboard'
import Orders from './pages/Orders'
import Settings from './pages/Settings'
import NotFound from './pages/NotFound'
import DashboardLayout from './layouts/DashboardLayout'
export default function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<Dashboard />} />
<Route path="orders" element={<Orders />} />
<Route path="settings" element={<Settings />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
)
}
Now /dashboard, /dashboard/orders, and /dashboard/settings all share the same layout.
This is one of those changes that makes a project feel professional very quickly.
How to migrate an existing project without breaking everything
If your app already has a lot of components, do not try to convert everything at once. That is usually where people get stuck.
A safer migration path looks like this:
Step 1: Keep the current UI working
Do not remove your existing screen logic immediately. First, add BrowserRouter and a small route setup.
Step 2: Move one screen at a time into pages
Pick one part of the app, wrap it in a route, and make sure it behaves correctly.
Step 3: Extract layouts
If you already have a navbar or sidebar, turn it into a layout component.
Step 4: Replace manual view switching
If you have code like this:
{activeTab === 'profile' && <Profile />}
{activeTab === 'billing' && <Billing />}
{activeTab === 'security' && <Security />}
consider turning those into real routes instead:
<Route path="/settings/profile" element={<Profile />} />
<Route path="/settings/billing" element={<Billing />} />
<Route path="/settings/security" element={<Security />} />
That change is often more powerful than it looks. It gives each section its own URL, improves navigation, and makes the app easier to maintain.
Step 5: Clean up old state-based navigation
Once routes are reliable, remove the extra state logic that no longer needs to control page-level UI.
This is the kind of migration that rewards patience. The app stays usable while you improve it piece by piece.
Basic navigation with Link and NavLink
In a real app, you should avoid using plain <a href="/..."> for internal navigation unless you truly need a full page reload.
Use Link instead.
Example
import { Link } from 'react-router-dom'
export default function Header() {
return (
<header>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/dashboard">Dashboard</Link>
</header>
)
}
Link changes the URL without reloading the page.
For navigation items, NavLink is often better because it can show the active state.
import { NavLink } from 'react-router-dom'
export default function MainNav() {
return (
<nav>
<NavLink
to="/"
className={({ isActive }) => (isActive ? 'active' : '')}
end
>
Home
</NavLink>
<NavLink
to="/about"
className={({ isActive }) => (isActive ? 'active' : '')}
>
About
</NavLink>
</nav>
)
}
That active state is very useful for sidebars, tabs, and menus.
Route parameters: building dynamic pages
Most real projects need dynamic routes. A blog, shop, CRM, or dashboard usually has pages like:
/products/12/users/5/orders/8391/blog/react-router-guide
React Router handles this with route parameters.
Route definition
<Route path="/products/:productId" element={<ProductDetails />} />
Reading the parameter
import { useParams } from 'react-router-dom'
export default function ProductDetails() {
const { productId } = useParams()
return (
<div>
<h1>Product Details</h1>
<p>Product ID: {productId}</p>
</div>
)
}
This is especially useful in an existing project where many screens may already be fetching data by ID. Routing makes that logic feel much more natural.
You can also use multiple params:
<Route path="/users/:userId/orders/:orderId" element={<OrderDetails />} />
And in the component:
import { useParams } from 'react-router-dom'
export default function OrderDetails() {
const { userId, orderId } = useParams()
return (
<div>
<h1>Order Details</h1>
<p>User: {userId}</p>
<p>Order: {orderId}</p>
</div>
)
}
Using search parameters for filters and sorting
Sometimes the path should stay the same, but the page state should change. That is where search parameters are useful.
For example:
/products?category=shoes/users?sort=latest/orders?status=pending
Reading search params
import { useSearchParams } from 'react-router-dom'
export default function ProductsPage() {
const [searchParams, setSearchParams] = useSearchParams()
const category = searchParams.get('category') || 'all'
const sort = searchParams.get('sort') || 'popular'
return (
<div>
<h1>Products</h1>
<p>Category: {category}</p>
<p>Sort: {sort}</p>
<button onClick={() => setSearchParams({ category: 'shoes', sort: 'latest' })}>
Show Shoes
</button>
</div>
)
}
Search params are excellent when the page is already part of your existing UI and you want the URL to reflect what the user is seeing. That makes the page easier to share and return to later.
Protected routes for authenticated sections
Most existing projects eventually need private pages. Maybe the app has a dashboard, admin area, account page, or internal tools. Those should not be open to everyone.
A common pattern is a protected route wrapper.
Example authentication gate
import { Navigate, Outlet } from 'react-router-dom'
export default function ProtectedRoute({ isAuthenticated }) {
if (!isAuthenticated) {
return <Navigate to="/login" replace />
}
return <Outlet />
}
Route usage
import { Routes, Route } from 'react-router-dom'
import ProtectedRoute from './components/ProtectedRoute'
import Login from './pages/Login'
import DashboardLayout from './layouts/DashboardLayout'
import Dashboard from './pages/Dashboard'
import Settings from './pages/Settings'
export default function App() {
const isAuthenticated = true
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route element={<ProtectedRoute isAuthenticated={isAuthenticated} />}>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<Dashboard />} />
<Route path="settings" element={<Settings />} />
</Route>
</Route>
</Routes>
)
}
This keeps route protection centralized instead of scattering checks across multiple components.
In a real project, isAuthenticated would likely come from context, Redux, cookies, or a session hook.
Redirects with Navigate
Redirects are common in existing apps. Maybe you want to send old URLs to new ones, or maybe a user should be moved after login.
Example: redirecting an old route
import { Navigate } from 'react-router-dom'
<Route path="/home" element={<Navigate to="/" replace />} />
Example: redirect after form submission
import { useNavigate } from 'react-router-dom'
export default function CreatePost() {
const navigate = useNavigate()
const handleSubmit = async (e) => {
e.preventDefault()
// save data here
navigate('/posts')
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Save Post</button>
</form>
)
}
useNavigate is one of those small tools that ends up being used everywhere once routing becomes part of the project.
Handling nested pages inside an existing dashboard
Let us say your existing project already has a dashboard with a lot of internal sections. You might have been using a sidebar and switching content with local state.
That works at first, but routing gives you a cleaner model.
Example dashboard structure
import { Routes, Route } from 'react-router-dom'
import DashboardLayout from './layouts/DashboardLayout'
import DashboardHome from './pages/dashboard/DashboardHome'
import UsersPage from './pages/dashboard/UsersPage'
import ReportsPage from './pages/dashboard/ReportsPage'
import UserDetails from './pages/dashboard/UserDetails'
export default function AppRoutes() {
return (
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="users" element={<UsersPage />} />
<Route path="users/:userId" element={<UserDetails />} />
<Route path="reports" element={<ReportsPage />} />
</Route>
</Routes>
)
}
Sidebar with links
import { NavLink, Outlet } from 'react-router-dom'
export default function DashboardLayout() {
return (
<div className="dashboard">
<aside className="sidebar">
<NavLink to="/dashboard" end>Overview</NavLink>
<NavLink to="/dashboard/users">Users</NavLink>
<NavLink to="/dashboard/reports">Reports</NavLink>
</aside>
<section className="content">
<Outlet />
</section>
</div>
)
}
This approach gives the user a much smoother experience. They can refresh, bookmark, and navigate naturally.
404 pages and fallback routes
Every project needs a friendly fallback.
Without one, users may land on a blank screen or a broken path and feel lost.
Example NotFound.jsx
import { Link } from 'react-router-dom'
export default function NotFound() {
return (
<div style={{ padding: 40, textAlign: 'center' }}>
<h1>404</h1>
<p>Sorry, the page you are looking for does not exist.</p>
<Link to="/">Go back home</Link>
</div>
)
}
Route usage
<Route path="*" element={<NotFound />} />
This small page protects the experience of the entire app. It is simple, but users notice when it is missing.
Lazy loading routes for better performance
As the existing project grows, routes can become heavy. If every page loads at once, the bundle size may get larger than necessary.
React lazy loading helps here.
Example
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
export default function App() {
return (
<Suspense fallback={<div>Loading page...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
)
}
This is especially helpful in existing apps that have many routes, admin screens, or feature-heavy sections. The idea is simple: only load what the user needs right now.
Data loading inside route pages
Once routing is in place, pages usually fetch data based on the current URL. That is one of the nicest parts of route-driven design.
Example product page
import { useParams } from 'react-router-dom'
import { useEffect, useState } from 'react'
export default function ProductDetails() {
const { productId } = useParams()
const [product, setProduct] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
let isMounted = true
async function fetchProduct() {
try {
setLoading(true)
const response = await fetch(`/api/products/${productId}`)
const data = await response.json()
if (isMounted) {
setProduct(data)
}
} finally {
if (isMounted) {
setLoading(false)
}
}
}
fetchProduct()
return () => {
isMounted = false
}
}, [productId])
if (loading) return <p>Loading...</p>
if (!product) return <p>Product not found.</p>
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
</div>
)
}
When you bring React Router into an existing project, many components become easier to understand because the URL now tells the app what to show.
Common mistakes when adding React Router to an existing project
A few problems show up again and again.
1. Forgetting to wrap with BrowserRouter
Without the router provider, links and hooks will fail.
2. Mixing old and new navigation patterns
If part of the app uses window.location, part uses state, and part uses router links, the experience becomes inconsistent.
3. Not planning layouts
Without layout routes, you may repeat the same header and sidebar across multiple pages.
4. Using a tags for internal links
This causes full page reloads and breaks the smooth SPA experience.
5. Putting too much logic in App.jsx
As the project grows, routing should become organized. Very large App.jsx files get hard to maintain.
6. Forgetting a catch-all route
Without a 404 page, invalid URLs become confusing.
7. Overcomplicating migration
You do not need to convert everything in one pass. That usually slows the team down.
The best way to avoid these issues is to move slowly and keep the app functional at every step.
A practical route setup for an existing project
Here is a realistic example of how you might organize a mature React app.
File structure
src/
layouts/
MainLayout.jsx
DashboardLayout.jsx
pages/
Home.jsx
About.jsx
Login.jsx
NotFound.jsx
dashboard/
DashboardHome.jsx
UsersPage.jsx
ReportsPage.jsx
UserDetails.jsx
components/
Header.jsx
Footer.jsx
Sidebar.jsx
ProtectedRoute.jsx
App.jsx
main.jsx
main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
)
App.jsx
import { Routes, Route } from 'react-router-dom'
import MainLayout from './layouts/MainLayout'
import DashboardLayout from './layouts/DashboardLayout'
import ProtectedRoute from './components/ProtectedRoute'
import Home from './pages/Home'
import About from './pages/About'
import Login from './pages/Login'
import NotFound from './pages/NotFound'
import DashboardHome from './pages/dashboard/DashboardHome'
import UsersPage from './pages/dashboard/UsersPage'
import UserDetails from './pages/dashboard/UserDetails'
import ReportsPage from './pages/dashboard/ReportsPage'
export default function App() {
const isAuthenticated = true
return (
<Routes>
<Route element={<MainLayout />}>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/login" element={<Login />} />
</Route>
<Route element={<ProtectedRoute isAuthenticated={isAuthenticated} />}>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="users" element={<UsersPage />} />
<Route path="users/:userId" element={<UserDetails />} />
<Route path="reports" element={<ReportsPage />} />
</Route>
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
)
}
MainLayout.jsx
import { Outlet, Link } from 'react-router-dom'
export default function MainLayout() {
return (
<div>
<header>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/dashboard">Dashboard</Link>
<Link to="/login">Login</Link>
</nav>
</header>
<main>
<Outlet />
</main>
</div>
)
}
This is the kind of structure that scales well in a real project. It is clean, readable, and easy for another developer to understand later.
Styling routes in a real application
Routing is not only about page switching. It also changes how your app feels.
A route-based project often needs:
active sidebar states
page transitions
consistent content spacing
layout-specific styles
responsive navigation
For example, a dashboard sidebar may highlight the current route, while a mobile menu may collapse into a drawer. Route state helps drive those UI decisions.
Here is a simple active link style:
import { NavLink } from 'react-router-dom'
export default function SidebarLink({ to, children }) {
return (
<NavLink
to={to}
end
className={({ isActive }) =>
isActive ? 'sidebar-link active' : 'sidebar-link'
}
>
{children}
</NavLink>
)
}
Once routes are part of the app, these details become much easier to manage.
Real-world example: converting tabbed sections into routes
This is one of the most practical upgrades you can make in an existing app.
Imagine a settings page with tabs:
Profile
Security
Notifications
Billing
Many apps start with local tab state:
const [tab, setTab] = useState('profile')
Then they render content based on that state. It works, but it has limits. The URL does not reflect the active section, and users cannot bookmark a specific tab.
A better approach is to turn the tabs into routes:
<Route path="/settings" element={<SettingsLayout />}>
<Route index element={<ProfileSettings />} />
<Route path="security" element={<SecuritySettings />} />
<Route path="notifications" element={<NotificationSettings />} />
<Route path="billing" element={<BillingSettings />} />
</Route>
Now each tab has a URL.
That simple change improves usability, makes the code easier to follow, and gives the app a more polished feel.
Example: a settings layout with nested routes
import { NavLink, Outlet } from 'react-router-dom'
export default function SettingsLayout() {
return (
<div style={{ display: 'grid', gridTemplateColumns: '220px 1fr', gap: 24 }}>
<aside>
<h2>Settings</h2>
<nav style={{ display: 'grid', gap: 10, marginTop: 16 }}>
<NavLink to="/settings" end>Profile</NavLink>
<NavLink to="/settings/security">Security</NavLink>
<NavLink to="/settings/notifications">Notifications</NavLink>
<NavLink to="/settings/billing">Billing</NavLink>
</nav>
</aside>
<section>
<Outlet />
</section>
</div>
)
}
This pattern is a great fit for existing projects because it creates order without forcing a redesign.
Testing route behavior
Once routing is part of the app, it is worth testing. Not every route needs a heavy test suite, but the critical paths should be reliable.
You may want to test:
public page rendering
protected route redirects
404 behavior
dynamic route rendering
navigation links
A routing test often checks that a page appears after rendering at a given route.
Example using React Testing Library:
import { render, screen } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import App from './App'
test('renders home page on root route', () => {
render(
<MemoryRouter initialEntries={['/']}>
<App />
</MemoryRouter>
)
expect(screen.getByText(/home/i)).toBeInTheDocument()
})
If your app is already mature, even a few route tests can prevent accidental regressions.
A thoughtful migration strategy for large projects
For bigger existing projects, the smartest move is often to migrate by section.
You might begin with:
public pages first
then authentication pages
then dashboard pages
then nested admin routes
then tabbed sections
finally, route cleanup and redirects
This phased approach keeps the team from feeling overwhelmed. It also reduces risk because each step can be reviewed and verified.
A good migration is less about speed and more about confidence.
If the app is already in production, that confidence matters a lot.
When not to overuse routing
Routing is powerful, but not every piece of UI deserves its own route.
For example, a modal, drawer, tooltip, dropdown, or simple accordion section usually should not become a route unless there is a real reason. Routes are best for meaningful destinations, not every state change.
Ask a simple question:
Does this deserve a URL?
If yes, route it. If not, keep it as component state.
That balance is one of the signs of a healthy React app.
A final example: routing in a content-driven project
Let us imagine an existing blog or content platform.
You may need routes like:
//blog/blog/:slug/categories/:categorySlug/author/:authorId/dashboard/posts/dashboard/posts/:postId/edit
A good router setup makes the structure clear:
<Routes>
<Route path="/" element={<MainLayout />}>
<Route index element={<Home />} />
<Route path="blog" element={<BlogList />} />
<Route path="blog/:slug" element={<BlogPost />} />
<Route path="categories/:categorySlug" element={<CategoryPage />} />
<Route path="author/:authorId" element={<AuthorPage />} />
</Route>
<Route element={<ProtectedRoute isAuthenticated={isAuthenticated} />}>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route path="posts" element={<PostsList />} />
<Route path="posts/:postId/edit" element={<EditPost />} />
</Route>
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
That structure gives the whole project a clear map. Once the map exists, adding new pages becomes much easier.
Final thoughts
Learning React Router in an existing project is really about learning how to evolve an app without breaking its shape.
The first step is technical, but the bigger skill is architectural. You are deciding what belongs in a route, what belongs in a layout, what belongs in page state, and what should stay simple. That kind of thinking makes a codebase healthier over time.
The nice thing about React Router is that it scales with you. It can handle a tiny site with three pages just as well as a large product with dashboards, nested sections, protected pages, and dynamic content. In an existing project, that flexibility is exactly what you need.
Start small. Add one route. Add one layout. Convert one tab system. Then keep going.