Navigation with Link and NavLink

When you build a multi-page experience with React Router, you don’t want every navigation click to reload the whole page from the server — that would throw away all your component state and defeat the purpose of a single-page application. The Link and NavLink components solve this: they render an accessible anchor tag under the hood, but intercept the click so React Router swaps in the matched route’s UI instead of asking the browser to fetch a new HTML document. NavLink is Link‘s specialized sibling for navigation menus — it knows whether its own destination is the currently active route, so you can style the "you are here" link differently from the rest.

Overview / How it works

Under the hood, Link renders a real anchor element (<a href="...">) so browsers, screen readers, and middle-click/"open in new tab" behavior still work as expected. It attaches an onClick handler that calls event.preventDefault() for a normal left-click without modifier keys, and instead updates the URL via the History API (through React Router’s internal history object) rather than letting the browser perform a full navigation. The router component that wraps your app is listening for that history change; when it fires, React Router re-evaluates your <Routes> tree, finds the <Route> whose path matches the new URL, and renders that route’s element. Because this happens inside React’s normal render cycle, only the parts of the component tree that actually change re-render — your <nav>, layout, and any state living outside the matched route are left untouched.

If the user middle-clicks, ctrl/cmd-clicks, or shift-clicks a Link, React Router deliberately does not call preventDefault(), so the browser’s native "open in new tab" behavior still works. This is one reason to always reach for Link instead of wiring a manual onClick plus useNavigate() onto a plain element — you would have to reimplement all of this accessibility and modifier-key behavior yourself.

NavLink wraps Link and adds one extra piece of logic: on every render it compares its own to path against the current URL (via useLocation() internally) and computes an isActive boolean (and an isPending boolean when used with a data router, for in-flight navigations). It then passes { isActive, isPending } into whichever of className, style, or children you supplied as a function, letting you style or render differently depending on whether that link represents the page the user is currently viewing.

By default, matching is prefix-based, not exact: NavLink to="/projects" is considered active for /projects, /projects/42, and /projects/42/edit. That’s usually what you want for a section link like "Projects", but it’s a common trap for a "Home" link pointing at /, because every route path starts with /. Pass the end prop to require an exact match instead.

Syntax

<Link to="/path" replace state={{ from: "nav" }} target="_blank">
  Link text or JSX
</Link>

<NavLink
  to="/path"
  end
  className={({ isActive, isPending }) => (isActive ? "active" : "")}
  style={({ isActive }) => ({ color: isActive ? "red" : "black" })}
>
  Link text or JSX
</NavLink>
Prop Component Description
to Link, NavLink Destination path (string) or a location object; required.
replace Link, NavLink If present, replaces the current history entry instead of pushing a new one (no back-button entry).
state Link, NavLink Arbitrary data attached to the navigation, readable via useLocation().state on the destination page.
target Link, NavLink Standard anchor target, e.g. _blank to open in a new tab.
reloadDocument Link, NavLink Forces a full browser navigation instead of client-side routing.
end NavLink only Requires an exact match (not prefix match) before isActive is true.
className / style NavLink only Can be a plain string/object, or a function receiving { isActive, isPending }.
children NavLink only Can also be a function receiving { isActive, isPending } to render different content when active.

Examples

Example 1: A basic navigation bar with Link

import { BrowserRouter, Routes, Route, Link } from "react-router-dom";

function Home() {
  return <h2>Home Page</h2>;
}

function About() {
  return <h2>About Page</h2>;
}

function Contact() {
  return <h2>Contact Page</h2>;
}

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/contact">Contact</Link>
      </nav>

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

export default App;

This renders a nav bar with three links. Clicking "About" updates the URL to /about and swaps in the About Page heading without a full page reload — the browser’s network tab shows no document request at all, only a URL bar change.

Example 2: Styling the active link with NavLink

import { BrowserRouter, Routes, Route, NavLink } from "react-router-dom";

function Home() {
  return <h2>Home Page</h2>;
}

function About() {
  return <h2>About Page</h2>;
}

function NavBar() {
  const linkClass = ({ isActive }) =>
    isActive ? "nav-link nav-link-active" : "nav-link";

  return (
    <nav>
      <NavLink to="/" end className={linkClass}>
        Home
      </NavLink>
      <NavLink to="/about" className={linkClass}>
        About
      </NavLink>
    </nav>
  );
}

function App() {
  return (
    <BrowserRouter>
      <NavBar />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

On /, the Home link renders with class nav-link nav-link-active while About renders with plain nav-link. Navigate to /about and the active class flips to the About link. Because end is set on the Home link, it stops being active as soon as the URL is anything other than exactly / — without end, Home would stay active on every route, since / is a prefix of every path.

Example 3: A generated sidebar with active styling and route state

import {
  BrowserRouter,
  Routes,
  Route,
  Link,
  NavLink,
  useLocation,
} from "react-router-dom";

const links = [
  { to: "/", label: "Dashboard" },
  { to: "/projects", label: "Projects" },
  { to: "/settings", label: "Settings" },
];

function Sidebar() {
  return (
    <nav>
      <ul>
        {links.map((link) => (
          <li key={link.to}>
            <NavLink
              to={link.to}
              end={link.to === "/"}
              style={({ isActive }) => ({
                fontWeight: isActive ? "bold" : "normal",
                color: isActive ? "crimson" : "black",
              })}
            >
              {link.label}
            </NavLink>
          </li>
        ))}
      </ul>
    </nav>
  );
}

function ProjectDetail() {
  const location = useLocation();
  const projectName = location.state?.name ?? "Untitled";
  return <h2>Now viewing: {projectName}</h2>;
}

function Projects() {
  return (
    <div>
      <h2>Projects</h2>
      <Link to="/projects/42" state={{ name: "Redesign Website" }}>
        Redesign Website
      </Link>
    </div>
  );
}

function App() {
  return (
    <BrowserRouter>
      <Sidebar />
      <Routes>
        <Route path="/" element={<h2>Dashboard</h2>} />
        <Route path="/projects" element={<Projects />} />
        <Route path="/projects/:id" element={<ProjectDetail />} />
        <Route path="/settings" element={<h2>Settings</h2>} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

The sidebar is generated from an array with .map(), and each NavLink gets a unique key so React can track list items correctly across re-renders. The active link is bolded and colored crimson via the style callback. Clicking "Redesign Website" navigates to /projects/42 and passes { name: "Redesign Website" } as route state; ProjectDetail reads it back with useLocation().state and renders "Now viewing: Redesign Website" — note this data lives only in browser history, not in the URL, so it disappears on a hard refresh.

How it works step by step

On click: the browser fires a click event on the underlying anchor. React’s synthetic event handler on Link checks whether the click was a plain left-click with no modifier keys and the link isn’t targeting a new tab; if so, it calls preventDefault() to stop the browser’s default full-page navigation, then calls the router’s internal navigate(), which pushes (or replaces) a new entry onto the History API stack.

On URL change: the router component (e.g. BrowserRouter) is subscribed to history changes. When the URL changes — whether from a Link click, useNavigate(), or the browser’s back/forward buttons — it triggers a re-render of the <Routes> tree. React Router matches the new URL against your route path patterns and renders the matching element. Every mounted NavLink also re-renders at this point, recomputing its own isActive value against the new URL and re-applying its className/style function.

On unmount: if the newly matched route no longer renders a component that was previously on screen, React unmounts it normally — running any cleanup functions in its useEffect hooks, just like unmounting for any other reason. Link and NavLink themselves don’t hold any special external subscriptions that need manual cleanup; they rely on the router context provided by BrowserRouter.

Common Mistakes

Mistake 1: Using a plain anchor tag instead of Link.

<a href="/about">About</a>

This works, but it triggers a full browser page reload: the entire app is torn down and rebuilt from scratch, all in-memory state is lost, and the request round-trips to the server even though your React app already has everything it needs client-side. Use Link so React Router intercepts the click:

<Link to="/about">About</Link>

Mistake 2: Forgetting the end prop on a root NavLink.

<nav>
  <NavLink to="/" className={({ isActive }) => (isActive ? "active" : "")}>
    Home
  </NavLink>
  <NavLink to="/about" className={({ isActive }) => (isActive ? "active" : "")}>
    About
  </NavLink>
</nav>

Because NavLink matching is prefix-based, to="/" matches every path in the app (they all start with /), so "Home" would appear active even while viewing /about. Add end so it only matches the exact root path:

<NavLink to="/" end className={({ isActive }) => (isActive ? "active" : "")}>
  Home
</NavLink>

Mistake 3: Mapping links without a key prop.

{links.map((link) => (
  <NavLink to={link.to}>{link.label}</NavLink>
))}

React needs a stable key on each element produced from an array to correctly track additions, removals, and reordering; without one you’ll see a console warning and risk subtle bugs if the list ever changes. Use a unique, stable value like the path itself:

{links.map((link) => (
  <NavLink key={link.to} to={link.to}>
    {link.label}
  </NavLink>
))}

Best Practices

  • Always use Link or NavLink for in-app navigation instead of a bare <a href>, so the browser doesn’t perform an unnecessary full-page reload.
  • Add the end prop to any NavLink whose to is an ancestor path of other routes (most commonly the root /), so it isn’t active everywhere.
  • Prefer the function form of className or style on NavLink over manually comparing useLocation().pathname yourself — it’s less code and handles trailing slashes and nested matches correctly.
  • Use state on Link to pass transient UI data (like where the user navigated from) rather than stuffing it into the URL, but don’t rely on it for anything that must survive a page refresh.
  • Reach for replace when a navigation shouldn’t create a new back-button entry, such as redirecting after a successful login or form submission.
  • Give every key in a mapped list of links a value derived from real, unique data (like the path) rather than the array index.

Practice Exercises

Exercise 1: Build a three-page app (Home, Blog, Contact) with a <nav> of Link components and matching <Route> entries. Confirm in the browser dev tools that clicking between pages doesn’t trigger a network request for a new document.

Exercise 2: Convert the nav bar from Exercise 1 to use NavLink with a className callback that adds an active class. Make sure the Home link only appears active when you’re exactly on /, not on every page.

Exercise 3: Create a list of "articles" (an array of { id, title } objects) and render a sidebar of Link components generated with .map(), each pointing to /articles/:id and passing the article’s title via state. On the article detail route, read the title back with useLocation().state and display it before the full article data has loaded.

Summary

  • Link renders a real anchor tag but intercepts normal clicks to perform client-side navigation instead of a full page reload.
  • Modifier-key clicks (ctrl/cmd/shift/middle-click) are left alone by Link, preserving native "open in new tab" behavior.
  • NavLink wraps Link and additionally computes isActive by comparing its to path to the current URL, exposing it to function-form className, style, or children.
  • Matching is prefix-based by default; pass end to require an exact match, which is almost always needed for a root / link.
  • Use state on Link/NavLink to pass transient data to the destination route via useLocation().state.
  • Always give a stable, unique key to links rendered from an array.