React Router 7 Basics: How Navigation Works in Modern React

Posted November 17, 2025 by Karol Polakowski

React Router 7 continues to center navigation around declarative route definitions, route-based data loading, and first-class programmatic APIs. This article walks through the core concepts you need to build reliable navigation in modern React apps — route objects, RouterProvider, Link/NavLink, useNavigate/useLocation, loaders/actions, nested routes with Outlet, and how to observe navigation state for transitions and UX.


Core concepts and the route object model

React Router uses a route object model rather than JSX-only routing. Instead of nesting many Route components in JSX, you define an array of route objects and mount them with a router provider. This approach powers features like data loading, route-level error handling, and better server integration.

Key pieces:

  • RouterProvider: Mounts the router at the app root.
  • createBrowserRouter (or createMemoryRouter/ createHashRouter): Builds a router from route objects.
  • Route objects: path, element, children, loader, action, errorElement, id, and more.
  • useNavigate, useLocation, Link, NavLink: Navigation primitives in components.

Example: creating a router

import { createBrowserRouter, RouterProvider } from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/",
    element: <AppLayout />,
    children: [
      { index: true, element: <Home /> },
      { path: "users", element: <Users /> },
      { path: "users/:id", element: <UserDetail /> },
    ],
  },
]);

function Root() {
  return <RouterProvider router={router} />;
}

Note: route children are nested and render via the Outlet component (covered below).

Declarative links vs programmatic navigation

  • Link and NavLink are preferred for declarative, accessible navigation in UI.
  • useNavigate gives imperative control for redirects, form responses, or conditional navigation.

Link and NavLink

import { Link, NavLink } from "react-router-dom";

function Nav() {
  return (
    <nav>
      <NavLink to="/" end>Home</NavLink>
      <NavLink to="/users">Users</NavLink>
    </nav>
  );
}

NavLink exposes an active state so you can style the active route.

useNavigate

import { useNavigate } from "react-router-dom";

function LoginButton() {
  const navigate = useNavigate();

  async function handleLogin() {
    // ... authenticate
    navigate('/dashboard', { replace: true }); // replace the current entry
  }

  return <button onClick={handleLogin}>Sign in</button>;
}

replace: true swaps the current history entry (useful after login to avoid back navigation to the login page).

Nested routes and Outlet

Nested routes let child routes render inside parent layouts. Parent route elements should include an Outlet to render the matched child route.

import { Outlet } from "react-router-dom";

function AppLayout() {
  return (
    <div>
      <Header />
      <main>
        <Outlet /> { /* child routes render here */ }
      </main>
    </div>
  );
}

Nested routing encourages composition: shared UI in parents, page-specific UI in children.


Data loading: loaders and actions

Modern React Router supports route-level data loading and mutations via loaders and actions. Loaders run before a route renders and supply data via useLoaderData. Actions are used for form submissions handled by the router.

Loader example

// route definition
{
  path: "users/:id",
  element: <UserDetail />,
  loader: async ({ params }) => {
    const res = await fetch(`/api/users/${params.id}`);
    if (!res.ok) throw new Response("Not found", { status: 404 });
    return res.json();
  },
}

// in component
import { useLoaderData } from "react-router-dom";

function UserDetail() {
  const user = useLoaderData();
  return <div>{user.name}</div>;
}

Loaders improve UX by centralizing fetch logic and enabling built-in pending/error states.


Observing navigation state and transitions

React Router exposes navigation state so you can show loading spinners, block transitions, or animate page changes.

  • useNavigation: inspect the current navigation (idle, loading, submitting).
  • useTransition (older name in some libs) / built-in patterns: use navigation state to coordinate UI.
import { useNavigation } from "react-router-dom";

function AppShell() {
  const navigation = useNavigation();
  const isLoading = navigation.state !== "idle";

  return (
    <div>
      {isLoading && <ProgressBar />}
      <Outlet />
    </div>
  );
}

Using navigation.state you can detect when a route is being loaded or a form submitted to avoid UI flicker.

Search params, location and query handling

  • useLocation gives the current location object: pathname, search, hash.
  • useSearchParams is a convenient wrapper to read and set URL query params.
import { useSearchParams } from "react-router-dom";

function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();
  const q = searchParams.get('q') || '';

  function updateQuery(nextQ) {
    setSearchParams({ q: nextQ });
  }

  // fetch or filter by `q`
}

Prefer useSearchParams for simple query-driven UI. For more control, parse location.search manually.


History behavior and scroll restoration

React Router integrates with the browser history. Key points:

  • navigate(‘/path’) pushes a new history entry; navigate(‘/path’, { replace: true }) replaces it.
  • Scroll restoration is handled by the router in many setups — you can customize it if needed.
  • For modal routes or nested overlays, consider keeping background location in state and rendering an overlay while the underlying page remains mounted.

Handling errors and 404s

Route objects support errorElement for per-route error handling and top-level error boundaries for global handling.

{
  path: "/users/:id",
  element: <UserDetail />,
  loader: userLoader,
  errorElement: <UserError />,
}

Errors thrown in loaders/actions will be rendered by the nearest errorElement.

Best practices and patterns

  • Favor route objects with RouterProvider for large apps — it centralizes routing logic.
  • Use loaders to fetch data where possible; they provide consistent loading and error semantics.
  • Use Link/NavLink for navigation from UI elements and useNavigate for procedural flows.
  • Keep route components focused; let parent layouts host shared UI and child routes render page content via Outlet.
  • Observe navigation.state to show progress indicators and avoid layout shifts.
  • Use nested routes for composition and predictable URL structure.

Quick migration tips (from older patterns)

  • If you used many Route components nested in JSX, consider moving to a route object array for clarity and to enable route-level loaders/actions.
  • Replace manual fetch-in-useEffect patterns with loaders where appropriate — they simplify SSR and avoid duplicate loading logic.

Summary

Modern React navigation with React Router centers on declarative route objects, explicit loaders/actions, and compact navigation primitives. That model makes it easier to reason about when data loads, how transitions behave, and how to compose nested UIs. Start with route objects and RouterProvider, use Link/NavLink for declarative navigation, and use loaders/actions to lift data logic into the router — then observe navigation state to deliver a polished UX.