React: Getting Started with React Router
Last updated: 2026-08-26
Tom is developing an e-commerce admin panel. Initially, he used
useStateto switch between "pages"—one state variável each for the home page, product list, and order details. As the number of pages grew to 10, the state logic became a tangled mess, and the URL bar remained stuck at/, making it impossible to directly share a link to a specific page. He realized: he was missing a professional routing solution.
1. What You'll Learn
- Criteria for Choosing Between BrowserRouter and HashRouter
- The path-matching mechanisms for "Routes" and "Route"
- Link and NavLink Declarative Navigation
- useParams: Reads dynamic URL parameters
- Nested Routes and Outlet Layout Mode
2. Conceptual Diagrams
flowchart LR
A[BrowserRouter<br/>Routing Container] --> B[Routes<br/>Routing Table]
B --> C["Route path='/'<br/>→ Home"]
B --> D["Route path='/products'<br/>→ ProductList"]
B --> E["Route path='/products/:id'<br/>→ ProductDetail"]
B --> F["Route path='*'<br/>→ NotFound"]
C --> G[Rendering Component]
D --> G
E --> G
F --> G
style A fill:#e1f5fe,stroke:#0288d1
style B fill:#fff3e0,stroke:#f57c00
style G fill:#e8f5e9,stroke:#388e3c
A user visits a different URL → BrowserRouter captures it → Routes match the most appropriate route → The corresponding component is rendered.
3. A Real-Life Scenario
Tom's admin panel needs three main pages: Dashboard, Product Management, and System Settings. In addition, the Product Management section includes two subpages: Product List and Product Details. He wants each page to have a unique URL so that users can navigate using their browser's forward and back buttons.
(1) Selecting a Routing Mode
React Router offers two routing modes, which differ fundamentally in how they handle URLs:
| Pattern | URL Example | Principle | Use Cases |
|---|---|---|---|
BrowserRouter |
example.com/users |
Manipulating URLs Using the History API | Projects Where the Server Can Configure URL Rewriting |
HashRouter |
example.com/#/users |
Preventing server requests triggered by changes to the URL hash | Static hosting (GitHub Pages, CDN) |
Recommendations: For projects that can be hosted on a server, always use BrowserRouter—it results in a cleaner URL and is more SEO-friendly. For static hosting, use HashRouter.
▶ Example 1: Basic Routing Configuration
Output:
Subheading: "Dashboard Home Page". routing via React Router
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'
function Home() {
return <h2>Dashboard Home Page</h2>
}
function ProductList() {
return <h2>Product List</h2>
}
function Settings() {
return <h2>System Settings</h2>
}
function NotFound() {
return <h2>404 — Page Not Found</h2>
}
function App() {
return (
<BrowserRouter>
<nav style={{ display: 'flex', gap: '1rem', padding: '1rem', background: '#f0f0f0' }}>
<Link to="/">Home</Link>
<Link to="/products">Product Management</Link>
<Link to="/settings">System Settings</Link>
</nav>
<main style={{ padding: '1rem' }}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<ProductList />} />
<Route path="/settings" element={<Settings />} />
<Route path="*" element={<NotFound />} />
</Routes>
</main>
</BrowserRouter>
)
}
export default App
Output:
URL routing: / → Home, /about → About, /contact → Contact. Navigation without page reload.
How It Works: When you click a navigation link, the URL changes and the page content updates, but the browser does not reload the entire page—this is the core experience of SPA routing.
About path="*": The wildcard * matches all paths not defined in preceding routes; it is typically placed at the end of the route list to generate a 404 page. In React Router v6, * can only appear as the final character of a path.
(2) NavLink Activation Status
| Navigation Component | Purpose | Activation Style | Use Cases |
|---|---|---|---|
<Link to="/path"> |
Declarative Navigation | None | General Navigation Links |
<NavLink to="/path"> |
Navigation with Active State | isActive Callback |
Sidebar/Top Navigation Highlight |
navigate('/path') |
Programmatic Redirects | — | Redirects after login, redirects after form submission |
<Navigate to="/path" /> |
Declarative Redirect | — | Conditional Redirect Component |
Tom wants to highlight the current page in the navigation bar so users can clearly see "where they are." The <NavLink> component provides the isActive parameter, which allows you to dynamically set styles based on the current route. It has two additional properties compared to <Link>: style and className both support accepting a callback function with the isActive and isPending properties.
▶ Example 2: Navigation Bar with Active Style
Output:
Subheading: "Dashboard Home Page". Displays: "Dashboard Home Page". React Router handles navigation
import { NavLink } from 'react-router-dom'
function NavBar() {
const linkStyle = {
padding: '8px 16px',
textDecoration: 'none',
borderRadius: '6px',
transition: 'all 0.2s'
}
const activeStyle = {
...linkStyle,
backgroundColor: '#1976d2',
color: '#fff',
fontWeight: 'bold'
}
const inactiveStyle = {
...linkStyle,
color: '#333'
}
return (
<nav style={{ display: 'flex', gap: '12px', padding: '12px', background: '#fafafa' }}>
<NavLink
to="/"
style={({ isActive }) => (isActive ? activeStyle : inactiveStyle)}
end // Exact Match,Avoid "/" Match all that start with "/" Starting path
>
Home
</NavLink>
<NavLink
to="/products"
style={({ isActive }) => (isActive ? activeStyle : inactiveStyle)}
>
Product Management
</NavLink>
<NavLink
to="/settings"
style={({ isActive }) => (isActive ? activeStyle : inactiveStyle)}
>
System Settings
</NavLink>
</nav>
)
}
Output:
NavBar component renders the described interactive UI
Key Point: The end property ensures that the / path is activated only when there is an exact match; otherwise, all paths will trigger the homepage’s activation style. NavLink’s className also supports callback functions, making it suitable for projects that use CSS class names.
(3) Sharing Nested Routes and Layouts
Tom discovered that the product management page contained two subpages—“Product List” and “Add Product”—which shared the same sidebar layout. Writing the layout code repeatedly for each subpage would be both redundant and difficult to maintain. React Router’s nested routing combined with <Outlet> perfectly solves this problem.
The core concept of nested routing is that the parent route defines the layout framework, and the child route injects content via outlets. The parent component does not concern itself with what the child route specifically renders; it is solely responsible for the layout framework.
▶ Example 3: Nested Routes and Outlets
Output:
Displays: "Home"
import { BrowserRouter, Routes, Route, Link, Outlet, useParams } from 'react-router-dom'
// Parent Layout Component — Shared Sidebar + Outlet
function ProductsLayout() {
return (
<div style={{ display: 'flex' }}>
<aside style={{ width: '200px', padding: '16px', background: '#f5f5f5' }}>
<h3>Product Management</h3>
<nav style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Link to="list">Product List</Link>
<Link to="add">Add Item</Link>
</nav>
</aside>
<main style={{ flex: 1, padding: '16px' }}>
{/* The components of the child route are rendered here */}
<Outlet />
</main>
</div>
)
}
function ProductList() {
const products = [
{ id: 1, name: 'React Programming Books', price: 79 },
{ id: 2, name: 'TypeScript Guide', price: 59 },
{ id: 3, name: 'Node.js Real-World Experience', price: 69 }
]
return (
<div>
<h2>Product List</h2>
<ul>
{products.map(p => (
<li key={p.id}>
<Link to={`/products/detail/${p.id}`}>
{p.name} — ${p.price}
</Link>
</li>
))}
</ul>
</div>
)
}
function AddProduct() {
return (
<div>
<h2>Add Item</h2>
<form onSubmit={e => { e.preventDefault(); alert('Submission Successful!') }}>
<div><label>Product Name:<input name="name" /></label></div>
<div><label>Price:<input name="price" type="number" /></label></div>
<button type="submit">Submit</button>
</form>
</div>
)
}
function ProductDetail() {
const { id } = useParams()
return <h2>Product Details(ID:{id})</h2>
}
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<h2>Home</h2>} />
{/* Nested Routes:Parent Route with Layout,Subnet routing is enabled Outlet Rendering */}
<Route path="/products" element={<ProductsLayout />}>
<Route index element={<ProductList />} /> {/* /products Default Display */}
<Route path="list" element={<ProductList />} /> {/* /products/list */}
<Route path="add" element={<AddProduct />} /> {/* /products/add */}
<Route path="detail/:id" element={<ProductDetail />} /> {/* /products/detail/1 */}
</Route>
<Route path="*" element={<h2>404 Not found</h2>} />
</Routes>
</BrowserRouter>
)
}
Output:
URL routing: / → Home, /about → About, /contact → Contact. Navigation without page reload.
Flow Logic: Visit /products/list → ProductsLayout (render the sidebar) → Outlet (display the ProductList component at that location). The URL structure corresponds one-to-one with the component structure, making it clear and easy to maintain.
About the index route: The index route is the default subroute for the parent path. When /products is accessed and no subpath matches (neither list nor add matches), the content of the index route appears in the Outlet. This ensures that the parent path does not display a blank area.
4. Dynamic Routing and Path Parameters
| Path Pattern | URL Example | useParams Return Value | Description |
|---|---|---|---|
/users/:id |
/users/42 |
{ id: '42' } |
Single Dynamic Parameter |
/users/:userId/posts/:postId |
/users/42/posts/99 |
{ userId: '42', postId: '99' } |
Multi-stage dynamic parameters |
/files/* |
/files/a/b/c |
{ '*': 'a/b/c' } |
Wildcard matching the remaining path |
/categories/:catId/:tab? |
Two routes must be defined | { catId, tab } |
Optional parameters (no native support) |
Dynamic routing is one of the most important features of a routing system. Tom’s product detail page needs to display different content based on the product ID, and he can’t possibly write a route for every product ID—this is where the :param syntax comes in to define dynamic paths.
(1) Using useParams
:id is a placeholder for a dynamic parameter; the actual value is extracted from the URL via the useParams hook.
function UserDetail() {
const { userId, postId } = useParams()
return <p>User {userId} Article {postId}</p>
}
// URL: /users/42/posts/99 → userId=42, postId=99
▶ Example 4: Product Details Page
Output:
Subheading: "Product List". Displays: "Product Management". Button: Submit. Input types: submit, number. Form with submit handling. React Router handles navigation
import { useParams, Link, useNavigate } from 'react-router-dom'
// Simulated Product Data
const products = [
{ id: '1', name: 'React Programming Books', price: 79, description: 'Mastering It from Scratch React 18 Development' },
{ id: '2', name: 'TypeScript Guide', price: 59, description: 'Systematic Study TypeScript Type System' },
{ id: '3', name: 'Node.js Real-World Experience', price: 69, description: 'Backend Development: From Beginner to Advanced' }
]
function ProductDetail() {
const { id } = useParams()
const navigate = useNavigate()
const product = products.find(p => p.id === id)
if (!product) {
return (
<div>
<h2>The product does not exist.</h2>
<button onClick={() => navigate('/products')}>Back to Product List</button>
</div>
)
}
return (
<div>
<h2>{product.name}</h2>
<p className="price">Price:${product.price}</p>
<p className="desc">{product.description}</p>
<Link to="/products">← Back to List</Link>
</div>
)
}
Output:
URL /users/42 → useParams() returns { id: "42" }. Dynamic route renders user profile for ID 42.
Note: When using dynamic parameters, you need to account for situations where "a parameter value has no corresponding data." The if (!product) branch in the code above handles empty data to prevent the page from displaying a blank screen when attempting to access a nonexistent ID.
(2) Multi-segment dynamic parameters
A path can contain multiple dynamic parameters, which is common in scenarios involving nested resources:
// Route Definitions
<Route path="/categories/:catId/products/:prodId" element={<ProductView />} />
// Extracted from the component
function ProductView() {
const { catId, prodId } = useParams()
// URL: /categories/electronics/products/42
// catId = "electronics", prodId = "42"
return <h2>Categories {catId} Items under {prodId}</h2>
}
(3) Optional Parameters and Wildcards
React Router v6 does not directly support optional parameters, but you can achieve a similar effect in two ways:
// Method 1:Define two Route(Recommendations)
<Route path="/categories/:catId" element={<CategoryPage />} />
<Route path="/categories/:catId/:tab" element={<CategoryPage />} />
// Method 2:Make the determination within the component itself
function CategoryPage() {
const { catId, tab } = useParams()
const activeTab = tab || 'overview' // Default value
return <h2>Categories {catId} - {activeTab}</h2>
}
The advantage of Option 1 is that the URL is semantically clear, while Option 2 is more concise but reduces the URL's readability.
(4) A Detailed Explanation of Path Matching Priorities
Path matching in React Router v6 is based on a scoring algorithm, rather than the "first-come, first-served" approach used in traditional frameworks. Understanding these rules can help troubleshoot issues where routes aren't working as expected.
// Suppose we have the following routing configuration
<Routes>
<Route path="/products/new" element={<NewProduct />} /> {/* Static Path */}
<Route path="/products/:id" element={<ProductDetail />} /> {/* Dynamic Path */}
<Route path="/products/:id/edit" element={<EditProduct />} /> {/* Hybrid Path */}
</Routes>
Match Priority (from highest to lowest):
- Static path segments (
new) take precedence over dynamic parameter segments (:id) - Paths with more static segments take precedence over paths with fewer static segments
- Paths with more segments take precedence over those with fewer segments.
Therefore, a request to /products/new matches <NewProduct />, a request to /products/42 matches <ProductDetail />, and a request to /products/42/edit matches <EditProduct />. Developers do not need to worry about the order—the system automatically selects the "best match."
❓ FAQ
/products/detail/1 from the server, but that file does not exist on the server. Solution: Configure try_files $uri $uri/ /index.html in Nginx to redirect all route requests to index.html, and let React Router handle the matching on the front end. If you cannot configure the server, switch to HashRouter (the portion after # will not be sent to the server).<Link> and the native <a> tag?<a> triggers a full page refresh in the browser, causing the SPA to lose all its in-memory state. <Link> prevents the default navigation, updates the URL via the History API, and notifies React Router to render a new component—without refreshing the page or losing state. In SPAs, always use Link or NavLink instead of <a>./users/new is more specific than /users/:id and therefore takes precedence. * acts as a wildcard that matches all unmatched paths and is typically placed last to serve as a 404 page. Paths use prefix matching by default; adding the end attribute changes this to exact matching.<Route index element={...} /> defines the default subroute for the parent route. When accessing the parent route itself (e.g., /products), if the parent route uses an Outlet, the content of the "index" route will be displayed in the Outlet. It acts as the "default page under the parent path," preventing a blank area from appearing when the parent path is accessed.<Routes> + <Route> replaces <Switch>, and the Route component automatically matches the most specific path; ② Nested routes now use <Outlet> instead of manually rendering child routes; ③ useNavigate() replaces useHistory()—navigate('/path') replaces history.push('/path'), and navigate(-1) replaces history.goBack(). The v6 API is more concise, but migrating requires significant code changes.(5) Relative Paths vs. Absolute Paths
In nested routing, the path for <Link to="..."> is relative to the current route, while <Link to="/..."> is an absolute path. Understanding this distinction is crucial for avoiding navigation errors.
// Currently in /products under (ProductsLayout Within the component)
<Link to="list"> {/* → /products/list(Relative Path,After appending it to the current route) */}
<Link to="/list"> {/* → /list(Absolute Path,Replace directly) */}
<Link to="../settings"> {/* → /settings(Parent-level relative path) */}
In nested routing, if a child route is located within ProductsLayout, all Links within it should use relative paths (without the leading /). This ensures that when the parent route’s path changes, the child route’s Links automatically adapt.
5. 404 Pages and Routing Design Patterns
(1) Wildcard Routing
path="*" matches all undefined routes and is the standard way to implement a 404 page. However, in React Router v6, * can only appear at the end of a route and cannot be used like path="/users/*/edit".
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<ProductList />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/about" element={<About />} />
{/* Wildcard routes must be placed last */}
<Route path="*" element={<NotFound />} />
</Routes>
)
}
function NotFound() {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<h1>404</h1>
<p>Sorry, the page you are looking for does not exist.。</p>
<Link to="/">Back to Home</Link>
</div>
)
}
(2) Basic Usage of useNavigate
Although useNavigate will be covered in detail in the advanced course, Tom will also need to use it in certain scenarios during the beginner stage—such as redirecting after a login page countdown or redirecting after a form submission.
import { useNavigate } from 'react-router-dom'
function OrderSuccess() {
const navigate = useNavigate()
const [countdown, setCountdown] = useState(5)
useEffect(() => {
const timer = setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
clearInterval(timer)
navigate('/orders') // Automatically redirect when the countdown ends
return 0
}
return prev - 1
})
}, 1000)
return () => clearInterval(timer)
}, [navigate])
return (
<div>
<h2>Order Placed Successfully!</h2>
<p>{countdown} You will be automatically redirected to the order page in seconds.</p>
<button onClick={() => navigate('/orders')}>Check it out now</button>
</div>
)
}
▶ Example 5: An Admin Panel Combining All Features
Output:
navigate("/dashboard") → programmatic redirect. navigate(-1) → go back. Used after login, form submit, etc.
Bring together all the concepts you’ve learned in this lesson—BrowserRouter, Routes, nested routes, NavLink, useParams, and 404 pages—to build the skeleton of a complete admin dashboard.
import { BrowserRouter, Routes, Route, NavLink, Outlet, useParams } from 'react-router-dom'
import './App.css'
// Layout Components
function AdminLayout() {
return (
<div className="admin-container">
<header className="admin-header">
<h1>Tom E-commerce Management Backend</h1>
</header>
<div className="admin-body">
<nav className="admin-sidebar">
<NavLink to="/" end>Dashboard</NavLink>
<NavLink to="/products">Product Management</NavLink>
<NavLink to="/orders">Order Management</NavLink>
<NavLink to="/settings">System Settings</NavLink>
</nav>
<main className="admin-content">
<Outlet />
</main>
</div>
</div>
)
}
// Page Components
function Dashboard() {
return <h2>Welcome back,Tom!Number of Orders Today:42</h2>
}
function ProductsLayout() {
return (
<div>
<h2>Product Management</h2>
<nav>
<NavLink to="list">Product List</NavLink>
<NavLink to="add">Add Item</NavLink>
</nav>
<Outlet />
</div>
)
}
function ProductList() {
return <p>The product list is displayed here...</p>
}
function AddProduct() {
return <p>The form for adding products is displayed here....</p>
}
function Orders() {
return <h2>Order Management</h2>
}
function Settings() {
return <h2>System Settings</h2>
}
function NotFound() {
return <h2>404 — Page Not Found</h2>
}
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<AdminLayout />}>
<Route index element={<Dashboard />} />
<Route path="products" element={<ProductsLayout />}>
<Route index element={<ProductList />} />
<Route path="list" element={<ProductList />} />
<Route path="add" element={<AddProduct />} />
</Route>
<Route path="orders" element={<Orders />} />
<Route path="settings" element={<Settings />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
)
}
Architectural Features: The entire application consists of only one <BrowserRouter> and one top-level <Routes>. AdminLayout serves as a global layout that embeds all page components via outlets. This "single route + nested layout" pattern is the standard architecture for medium-sized React applications.
📖 Summary
- BrowserRouter is based on the History API, while HashRouter is based on URL hashes; the former is recommended (requires server support)
- The Routes component handles matching; the Route component defines the mapping between paths and components
- Link: For declarative navigation; NavLink provides active state detection (isActive callback)
- useParams extracts dynamic parameters from the URL and supports multi-segment parameters (
:id,:catId/:prodId) - Nested routes use outlets to reuse layouts, and the "index" route provides the default content for the parent path
- When deploying BrowserRouter in a production environment, you must configure server URL rewrite rules.
- In nested routing, be aware of the difference between relative paths (without the leading
/) and absolute paths (with the leading/)
📝 Exercises
- Create a 4-page admin dashboard: Home, User List, User Details (retrieve the user ID using
useParams), and About page. UseLinkfor navigation, and ensure thatNavLinkhas an active style. - Add nested routes to the user list page:
/usersdisplays the user list, and/users/:iddisplays user details; both share a layout component with a header (using an Outlet). - Add a 404 page to the end of the routing table to display a user-friendly message when a user accesses a nonexistent path.