Skip to content

React Router — Declarative Routing for React Single-Page Applications

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about React Router. We cover key concepts, practical examples, and best practices to help you master this topic.

React Router provides declarative, component-based routing for React SPAs with nested routes, URL parameters, Lazy Loading, and navigation guards for production applications.

What You'll Learn

By the end of this tutorial, you will understand how to set up React Router, define routes with parameters, handle navigation, implement nested layouts, and protect routes with authentication guards.

Why It Matters

React Router is the de facto standard routing library for React applications with over 10 million weekly npm downloads. Mastering it is essential for building any non-trivial React SPA that needs multiple views, URL-based navigation, and deep linking.

Real-World Use

A dashboard SPA uses React Router v6 with nested routes: a main layout containing a sidebar, header, and content area. The routes /dashboard, /dashboard/users, and /dashboard/settings all render within the layout. URL parameters like /dashboard/users/123 load specific user profiles.

React Router Setup

React Router Architecture
    BrowserRouter (listens to URL changes)
         ↓
    Routes (matches URL to route definition)
         ↓
    Route (renders component when path matches)
         ↓
    Layout Component (shared UI: nav, sidebar)
         ↓
    Outlet (renders child route content)
         ↓
    Link / NavLink (client-side navigation)

Think of React Router like a GPS navigation system for your app. You define possible routes (destinations), and when the URL changes (you move), React Router guides the rendering (shows the right view). Nested routes are like navigating into sub-regions.

Basic Setup

// App.js
import { BrowserRouter, Routes, Route, Link, Outlet } from 'react-router-dom';

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 />} />
                <Route path="*" element={<NotFound />} />
            </Routes>
        </BrowserRouter>
    );
}

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

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

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

function NotFound() {
    return <h1>404  Page Not Found</h1>;
}

Nested Routes with Layouts

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

// Layout component wraps child routes
function DashboardLayout() {
    return (
        <div className="dashboard">
            <aside className="sidebar">
                <Link to="/dashboard">Overview</Link>
                <Link to="/dashboard/users">Users</Link>
                <Link to="/dashboard/settings">Settings</Link>
            </aside>
            <main className="content">
                <Outlet /> {/* Child route renders here */}
            </main>
        </div>
    );
}

function App() {
    return (
        <BrowserRouter>
            <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/dashboard" element={<DashboardLayout />}>
                    <Route index element={<DashboardOverview />} />
                    <Route path="users" element={<UsersList />} />
                    <Route path="users/:userId" element={<UserDetail />} />
                    <Route path="settings" element={<Settings />} />
                </Route>
                <Route path="*" element={<NotFound />} />
            </Routes>
        </BrowserRouter>
    );
}

Route Parameters

import { useParams, useSearchParams } from 'react-router-dom';

// URL parameter: /users/123
function UserDetail() {
    const { userId } = useParams();
    const [user, setUser] = useState(null);

    useEffect(() => {
        fetch(`/api/users/${userId}`)
            .then(res => res.json())
            .then(setUser);
    }, [userId]);

    if (!user) return <div>Loading user {userId}...</div>;

    return (
        <div>
            <h1>{user.name}</h1>
            <p>Email: {user.email}</p>
        </div>
    );
}

// Query parameters: /search?q=react&page=2
function SearchPage() {
    const [searchParams, setSearchParams] = useSearchParams();

    const query = searchParams.get('q') || '';
    const page = parseInt(searchParams.get('page') || '1');

    function updateQuery(newQuery) {
        setSearchParams({ q: newQuery, page: '1' });
    }

    return (
        <div>
            <input
                value={query}
                onChange={(e) => updateQuery(e.target.value)}
                placeholder="Search..."
            />
            <p>Search results for "{query}", page {page}</p>
        </div>
    );
}

Programmatic Navigation

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

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

    async function handleSubmit(event) {
        event.preventDefault();
        try {
            await login(event.target.email.value, event.target.password.value);
            // Navigate to dashboard after login
            navigate('/dashboard', { replace: true });
        } catch (error) {
            // Stay on login, show error
            setError('Invalid credentials');
        }
    }

    return (
        <form onSubmit={handleSubmit}>
            <input name="email" type="email" required />
            <input name="password" type="password" required />
            <button type="submit">Login</button>
            <p>
                <a href="/register" onClick={(e) => {
                    e.preventDefault();
                    navigate('/register');
                }}>
                    Create account
                </a>
            </p>
        </form>
    );
}

Protected Routes

import { Navigate, useLocation } from 'react-router-dom';

// Protected route component
function RequireAuth({ children }) {
    const { user } = useAuth(); // Custom auth hook
    const location = useLocation();

    if (!user) {
        // Redirect to login, but remember where they wanted to go
        return <Navigate to="/login" state={{ from: location }} replace />;
    }

    return children;
}

// Usage in routes
function App() {
    return (
        <BrowserRouter>
            <Routes>
                <Route path="/login" element={<Login />} />
                <Route path="/register" element={<Register />} />
                <Route path="/dashboard" element={
                    <RequireAuth>
                        <DashboardLayout />
                    </RequireAuth>
                }>
                    <Route index element={<Overview />} />
                    <Route path="settings" element={<Settings />} />
                </Route>
            </Routes>
        </BrowserRouter>
    );
}

// Redirect back after login
function Login() {
    const navigate = useNavigate();
    const location = useLocation();

    const from = location.state?.from?.pathname || '/dashboard';

    async function handleLogin(event) {
        event.preventDefault();
        await login(event.target);
        navigate(from, { replace: true });
    }

    return (
        <form onSubmit={handleLogin}>
            <input name="email" type="email" required />
            <input name="password" type="password" required />
            <button type="submit">Login</button>
            <p>Redirected from: {from}</p>
        </form>
    );
}

Lazy Loading Routes

import { lazy, Suspense } from 'react';

// Lazy load page components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Users = lazy(() => import('./pages/Users'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
    return (
        <BrowserRouter>
            <Suspense fallback={<div className="loading">Loading page...</div>}>
                <Routes>
                    <Route path="/" element={<Home />} />
                    <Route path="/dashboard" element={<Dashboard />} />
                    <Route path="/users" element={<Users />} />
                    <Route path="/settings" element={<Settings />} />
                </Routes>
            </Suspense>
        </BrowserRouter>
    );
}

Common Mistakes

  1. Not wrapping routes in Suspense with lazy loading. React Router does not automatically handle loading states for lazy-loaded routes. Always provide a Suspense fallback.
  2. Forgetting the index route for nested layouts. The index route renders when the parent path matches exactly. Without it, the Outlet renders nothing.
  3. Using tags instead of . Regular anchor tags cause full page reloads in SPAs. Always use Link or NavLink for internal navigation.
  4. Not handling the wildcard route. Always include a Route with path="*" to catch unmatched routes and show a 404 page.
  5. Overusing useNavigate for simple links. Use the Link component for navigation. Reserve useNavigate for programmatic navigation after async operations.

Practice Questions

  1. What is the difference between BrowserRouter and HashRouter in React Router?
  2. How do nested routes work with the Outlet component?
  3. How do you access URL parameters and query parameters?
  4. How do you implement authentication guards for routes?
  5. How does lazy loading work with React Router?

Challenge: Build a React Router v6 application with: a public section (home, about), an authenticated dashboard with nested layout (overview, users/:id, settings), lazy-loaded pages, login redirect with return URL, and a 404 catch-all.

FAQ

Which version of React Router should I use?

Use React Router v6 (current stable). v5 is legacy. v6 has a simpler API with relative routes, nested routing built-in, and better TypeScript support.

Can I use React Router without create-react-app?

Yes. React Router works with any React setup: Vite, Next.js, or manual Webpack config. Install react-router-dom and wrap your app in BrowserRouter.

Does React Router support scroll restoration?

React Router v6 does not automatically restore scroll position on navigation. Use the useScrollRestoration hook or a third-party library.

{{< faq "How do I pass props to route components?" "Pass props using the element prop: <Route path="/user" element={} />. Do not use render props from v5." >}}

Can I use React Router with TypeScript?

Yes. React Router v6 has built-in TypeScript definitions. Use typed params with useParams<{userId: string}>() for type-safe routing.

Mini Project

Build a multi-page React SPA with React Router v6: a landing page, a blog section with article list and article detail routes, an admin dashboard with nested routes (statistics, posts, settings), authentication guard on admin routes, lazy loading for dashboard pages, and a 404 page.

What's Next

You mastered React Router. Now learn Vue Router for Vue.js SPAs, or explore state management patterns for managing application data across routes.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro