Skip to main content

React Router: Routes, Route, and Link

Client-side routing is the foundation of modern single-page applications. React Router v6+ provides three core components—<Routes>, <Route>, and <Link>—that enable declarative URL-based navigation without full page reloads. This guide covers how these components work together to build responsive, multi-page UIs in React.

What Are the Core React Router Components?

React Router provides these three essential components for routing:

<Routes> is a container that matches the current URL against a list of <Route> definitions. It renders the first route whose path matches the URL. If no routes match, nothing is rendered. Think of it as a switch statement for URLs.

<Route> defines a single route by pairing a URL path (e.g., /about) with a component to render (e.g., <About />). When the URL matches the route's path, React Router renders that component.

<Link> creates clickable navigation links (similar to <a> tags) that change the URL without triggering a full page reload. This preserves component state and enables the smooth, fast experience users expect from single-page apps.

How Do You Define Routes in React Router?

Start by creating page components:

// Home.jsx
export default function Home() {
return <h1>Welcome Home</h1>;
}

// About.jsx
export default function About() {
return <h1>About Our App</h1>;
}

// Contact.jsx
export default function Contact() {
return <h1>Contact Us</h1>;
}

Then define routes in your App.jsx:

import { Routes, Route } from 'react-router-dom';
import Home from './Home';
import About from './About';
import Contact from './Contact';

export default function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
);
}

When the browser URL is /about, React Router matches it to the second route and renders <About />. Each route has a path (the URL to match) and an element (the component to display).

Use <Link> components to navigate between routes without full page reloads:

import { Routes, Route, Link } from 'react-router-dom';
import Home from './Home';
import About from './About';
import Contact from './Contact';

export default function App() {
return (
<div>
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/contact">Contact</Link></li>
</ul>
</nav>

<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</div>
);
}

The to prop specifies where to navigate. Clicking a <Link> updates the URL and renders the matching route's component. No page refresh occurs; component state is preserved across navigation.

<Link> components render as <a> tags but intercept clicks to prevent default browser behavior (full page reload). Instead, React Router updates the URL in the browser history and re-renders the matching route.

Use <Link> for internal app navigation. Use <a> only for external URLs:

// Internal navigation — use Link
<Link to="/about">About Page</Link>

// External link — use <a>
<a href="https://example.com">External Site</a>

Organizing Routes for Scalability

For larger apps, organize routes in a configuration file:

// routes.js
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';

export const routes = [
{ path: '/', element: <Home /> },
{ path: '/about', element: <About /> },
{ path: '/contact', element: <Contact /> },
];

// App.jsx
import { Routes, Route } from 'react-router-dom';
import { routes } from './routes';

export default function App() {
return (
<Routes>
{routes.map((route) => (
<Route key={route.path} path={route.path} element={route.element} />
))}
</Routes>
);
}

This pattern scales to dozens of routes without making App.jsx unwieldy.

Handling Unmatched Routes

Use a catch-all route with path="*" to handle URLs that don't match any defined route:

import NotFound from './pages/NotFound';

<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>

The * route must come last because <Routes> matches in order. If it's first, it will match every URL.

Best Practices for React Router

Place BrowserRouter High in the Tree: Wrap your app with <BrowserRouter> near the root (in main.jsx or a layout component) so all routes and navigation links can access router context.

Keep Routes and Navigation Together: Define routes and navigation in the same component or a shared layout. This keeps the routing structure visible and maintainable.

Use Semantic Path Names: Keep URLs descriptive and lowercase. /user-profile is clearer than /up or /userprofile123.

Nest Routes Logically: Group related routes together. All admin routes could be under /admin/*, all blog routes under /blog/*.

Anti-Patterns to Avoid

Do Not Manually Change the URL: Never modify window.location or window.history directly. Let React Router manage the URL via <Link> or the useNavigate hook.

Do Not Forget to Wrap Your App with BrowserRouter: Routes and navigation won't work without <BrowserRouter> at the root level.

Do Not Use Anchor Tags for Internal Navigation: Using <a href="/about"> causes a full page reload and loses all component state. Always use <Link>.

Key Takeaways

  • <Routes> is a container that matches the current URL against <Route> definitions and renders the first matching route.
  • <Route> pairs a URL path with a component; when the URL matches the path, that component renders.
  • <Link> provides navigation between routes without full page reloads, preserving app state and enabling the smooth single-page app experience.
  • <Link> uses the to prop (instead of href) to specify the target path.
  • A catch-all path="*" route handles unmatched URLs; place it last in your <Routes> list.

Frequently Asked Questions

<Route> defines which component renders for a given URL path. <Link> is a clickable element that navigates to a path. Routes define what; Links trigger navigation.

Ensure your entire app is wrapped in <BrowserRouter> (typically in main.jsx). <Link> components can only work within a router context. If BrowserRouter is missing, clicks do nothing.

Can I have multiple <Routes> in my app?

Yes. Each <Routes> independently matches against the current URL. You might have one main <Routes> for pages and another for modals or sidebars. Each renders matching components independently.

How do I pass data to a routed component?

Use URL parameters (/user/:id) to encode identifiers, then access them with useParams(). For complex data, lift state to a parent component or use a state management library like Redux.

What happens if two routes have the same path?

The first matching route wins. <Routes> stops at the first match, so order matters. More specific routes (e.g., /user/profile) should come before generic ones (e.g., /user/:id).

Further Reading