React

React Router

15 Questions

React Router is the standard routing library for React. Wrap your application root with BrowserRouter to enable client-side routing using the HTML5 History API — navigating between pages updates the URL without triggering a full page reload. BrowserRouter uses pushState and replaceState to keep the URL in sync with the component tree. This requires your web server to serve the same index.html for all routes, otherwise direct links will return server-side 404 errors.
import { BrowserRouter } from 'react-router-dom';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);
BrowserRouter uses clean URLs without hash fragments. For static hosting that doesn't support URL rewriting, use HashRouter instead.

Why it matters: BrowserRouter enables client-side navigation so users can move between pages without a full page reload.

Real applications: Any multi-page React app — dashboards, e-commerce sites, admin panels — all use a router at the top level.

Common mistakes: Wrapping only part of the app in BrowserRouter instead of the entire app, breaking navigation in other branches.

Routes is a container that looks at the current URL and renders the first Route whose path matches. Each Route maps a URL path to a component via the element prop. React Router v6 uses exclusive matching by default — only the first matching route renders, so route order matters. Use path="*" as the last route to catch all unmatched URLs as a 404 fallback.
import { Routes, Route } from 'react-router-dom';

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/contact" element={<Contact />} />
    </Routes>
  );
}
In React Router v6, Routes replaces Switch and uses the element prop instead of component or render.

Why it matters: Routes map URL paths to components, so the right page shows for each URL without any manual conditionals.

Real applications: Defining a home page, about page, product detail page, and 404 page in a single route config.

Common mistakes: Using the old v5 component prop in v6 — always use element={<Component />} in React Router v6.

Link renders an <a> tag that navigates to a new route without a full page reload, using the router's History API. NavLink works identically but automatically applies an active CSS class when the current URL matches, making it ideal for navigation menus where the current page link should be visually highlighted. Never use a native <a href> for internal navigation — it triggers a full reload and resets all React state.
import { Link, NavLink } from 'react-router-dom';

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

<NavLink
  to="/about"
  className={({ isActive }) => isActive ? "active" : ""}
>
  About
</NavLink>
Use NavLink for navigation menus where you need visual feedback on the current page. Use Link for general in-app links.

Why it matters: Both prevent full page reloads. NavLink adds the "active" class automatically, so you don't have to track the current route manually.

Real applications: Sidebar or top nav menus that highlight the current page use NavLink; breadcrumbs and content links use Link.

Common mistakes: Using a plain <a> tag for internal links — it reloads the page and breaks the single-page app experience.

useNavigate returns a programmatic navigation function for redirecting users in response to logic — such as after a successful form submission, a failed auth check, or any async operation. Call navigate('/path') to push a new entry onto the history stack, or pass { replace: true } to replace the current entry. Pass navigate(-1) to go back one step, equivalent to the browser back button. This is the imperative counterpart to the declarative <Navigate> component.
import { useNavigate } from 'react-router-dom';

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

  const handleLogin = () => {
    // ... perform login logic
    navigate('/dashboard');          // push
    navigate('/dashboard', { replace: true }); // replace history
    navigate(-1);                    // go back
  };
}
Use replace: true for redirects where the user shouldn't go back to the previous page (e.g., after login).

Why it matters: Programmatic navigation lets you redirect users after an action like submitting a form or logging in.

Real applications: Redirecting to a dashboard after login, going back after cancelling an edit, navigating after a successful form submit.

Common mistakes: Calling navigate inside a render without a condition — this causes an infinite navigation loop.

useParams reads the dynamic URL segments of the current route and returns them as a key-value object. For a route defined as /product/:id, calling useParams() on the URL /product/42 returns { id: '42' }. All values are strings — parse them with Number(id) before using as API parameters. Multiple dynamic segments in one path (/user/:userId/post/:postId) are all returned in the same object.
// Route definition
<Route path="/users/:userId" element={<UserProfile />} />

// Component
import { useParams } from 'react-router-dom';

function UserProfile() {
  const { userId } = useParams();
  return <h1>User ID: {userId}</h1>;
}
Parameters are always strings. Convert them to numbers or other types as needed. Use multiple params like /posts/:postId/comments/:commentId.

Why it matters: URL parameters let you build dynamic pages where the content changes based on what's in the URL.

Real applications: User profile pages (/users/:id), product detail pages (/products/:slug), blog posts (/posts/:postId).

Common mistakes: Forgetting that params are strings — doing math with a param without converting it to a number first will give wrong results.

Nested routes allow child components to render inside a parent route's layout by nesting <Route> elements and placing an <Outlet /> in the parent's JSX where the child should appear. This is the standard way to build dashboards with a persistent sidebar: the parent renders the shell, and the matching child fills the outlet. Index routes (using the index prop) define what renders in the outlet when no child route is matched.
function App() {
  return (
    <Routes>
      <Route path="/dashboard" element={<DashboardLayout />}>
        <Route index element={<Overview />} />
        <Route path="settings" element={<Settings />} />
        <Route path="profile" element={<Profile />} />
      </Route>
    </Routes>
  );
}

function DashboardLayout() {
  return <div><Sidebar /><Outlet /></div>;
}
The index route renders when the parent path matches exactly. Nested routes keep layouts consistent across related pages.

Why it matters: Nested routes let child pages share a common layout (like a sidebar or tabs) without repeating markup.

Real applications: A settings page with tabs (Profile, Security, Billing) where each tab is a nested route under /settings.

Common mistakes: Forgetting the <Outlet /> in the parent layout — without it, child routes render nowhere.

Dynamic segments are defined by prefixing a parameter name with a colon in the route path (:id, :slug, :userId). They act as wildcards that match any non-slash value at that URL position and are accessible via useParams(). Multiple dynamic segments can coexist in one path (/team/:teamId/player/:playerId). React Router also supports optional segments (:id?) and splat patterns (*, matches the rest of the path) for flexible URL structures.
<Routes>
  <Route path="/products/:category" element={<ProductList />} />
  <Route path="/products/:category/:productId" element={<ProductDetail />} />
</Routes>

function ProductDetail() {
  const { category, productId } = useParams();
  return <p>{category} — Product #{productId}</p>;
}
Dynamic routes are essential for pages like user profiles, product details, or any content driven by URL parameters.

Why it matters: Dynamic routes let you build one component that works for many different URLs, reading the ID or slug from the URL with useParams.

Real applications: Product pages, article pages, user profiles, order detail pages.

Common mistakes: Hardcoding IDs in route paths instead of using a dynamic segment like :id.

A protected route is a wrapper component that checks an authentication condition before deciding what to render. If authenticated, it renders the children or an <Outlet>; if not, it redirects to login using <Navigate to="/login" replace />. The replace option ensures the redirect replaces the history entry so users can't back-button into the protected page. Store the original path in location state so the app can redirect back after a successful login.
function ProtectedRoute({ children }) {
  const { isAuthenticated } = useAuth();
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }
  return children;
}

// Usage in routes
<Route path="/dashboard" element={
  <ProtectedRoute><Dashboard /></ProtectedRoute>
} />
Use the replace prop on Navigate so the login redirect doesn't pollute the browser history.

Why it matters: Protected routes block unauthenticated users from seeing private pages and redirect them to login instead.

Real applications: Protecting dashboard, profile, and settings pages so only logged-in users can access them.

Common mistakes: Checking authorization inside the page component instead of in a wrapper — the page briefly renders before the redirect happens.

A catch-all route with path="*" is placed as the last route in a <Routes> block. React Router's exclusive matching means it only renders when no previous route matched the current URL. It renders a friendly 404 page with navigation options rather than a blank screen. Within nested route groups you can have separate catch-all routes at each level to handle unmatched paths within specific app sections.
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  <Route path="*" element={<NotFound />} />
</Routes>

function NotFound() {
  return <h1>404 — Page Not Found</h1>;
}
The * wildcard matches any path not previously matched. Place it last since React Router v6 uses ranking-based matching.

Why it matters: Without a 404 route, unmatched URLs show a blank page with no feedback, which is a poor user experience.

Real applications: Any production app needs a 404 page for broken links, mistyped URLs, or removed content.

Common mistakes: Placing the wildcard route before other routes — it will match everything and nothing else will ever render.

useLocation returns the current URL's location object with pathname (path segment), search (query string with the ?), hash (anchor with the #), and state (data passed via navigation). The component re-renders whenever the URL changes, making it useful for triggering data fetches on navigation, tracking analytics page views, or reading data passed between pages. For query parameters, use new URLSearchParams(location.search) or the dedicated useSearchParams hook.
import { useLocation } from 'react-router-dom';

function Analytics() {
  const location = useLocation();

  useEffect(() => {
    trackPageView(location.pathname);
  }, [location]);

  return null;
}

// Passing state during navigation
navigate('/checkout', { state: { from: '/cart' } });
const { state } = useLocation(); // { from: '/cart' }
useLocation is commonly used for analytics tracking, reading query parameters, or accessing navigation state.

Why it matters: useLocation gives you the current URL info so you can react to route changes, read query strings, or send page views to analytics.

Real applications: Sending a page view event to analytics on each route change, showing a "you came from X" message, reading filter values from the URL.

Common mistakes: Reading location.search as a raw string and parsing it manually instead of using useSearchParams.

useSearchParams works like useState but for the URL query string — it returns the current URLSearchParams object and a setter that updates the URL. Use searchParams.get('key') to read a value and call setSearchParams({ key: 'value' }) to update it, adding a new browser history entry. Query params are ideal for shareable UI state like search filters, sort order, and pagination that should survive a refresh or be bookmarkable. Unlike route params, they are fully optional and can be added without changing the route definition.
import { useSearchParams } from 'react-router-dom';

function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();
  const category = searchParams.get('category') || 'all';
  const page = Number(searchParams.get('page')) || 1;

  const changeCategory = (cat) => {
    setSearchParams({ category: cat, page: 1 });
  };

  return (
    <>
      <button onClick={() => changeCategory('shoes')}>Shoes</button>
      <button onClick={() => changeCategory('bags')}>Bags</button>
      <p>Showing: {category}, Page: {page}</p>
    </>
  );
}
setSearchParams updates the URL query string without a full page reload, keeping filters bookmarkable.

Why it matters: Query parameters let users bookmark or share filtered/searched views with specific state encoded in the URL.

Real applications: Search results pages, filter panels, pagination — all encoded as URL query strings.

Common mistakes: Managing filter state in useState only — the URL doesn't update so users can't share or bookmark the result.

Navigation state lets you pass data to a destination route without encoding it in the URL. Pass the data as the state option in navigate() or <Link state={...}>, and read it on the destination page with useLocation().state. The state lives in the browser history entry and disappears on page refresh, so always write defensive code on the receiving page since the state may be null if the user visits the URL directly. Use this for transient data like confirmation details or previous page context.
// Source page — pass data while navigating
const navigate = useNavigate();
navigate('/order-confirmation', {
  state: { orderId: 'ORD-123', total: 49.99 }
});

// Destination page — read the passed state
function OrderConfirmation() {
  const { state } = useLocation();
  return (
    <div>
      <h1>Order #{state?.orderId}</h1>
      <p>Total: ${state?.total}</p>
    </div>
  );
}
Navigation state is not in the URL, so it disappears on page refresh. Only use it for transient data like a success message. For persistent data use URL params or context.

Why it matters: Navigation state lets you pass lightweight data between routes without putting it in the URL or in global state.

Real applications: Showing a "Payment successful!" message on the confirmation page, pre-filling a form with data from the previous page.

Common mistakes: Storing important data in navigation state — it's lost on page refresh, so it's only suitable for transient messages.

A layout route is a parent route with no path that renders a shared UI shell (header, sidebar, footer) and an <Outlet> where child routes appear. The layout renders once and persists across child navigations — only the outlet content swaps without re-mounting the frame. This is the standard pattern for dashboards and authenticated sections that share a consistent layout. Define multiple layout routes at the same level to give different sections completely different shells.
function AppLayout() {
  return (
    <>
      <Navbar />
      <main>
        <Outlet /> {/* child route renders here */}
      </main>
      <Footer />
    </>
  );
}

function App() {
  return (
    <Routes>
      <Route element={<AppLayout />}>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
      </Route>
    </Routes>
  );
}
The layout route has no path prop — it wraps child routes purely for shared UI like a navbar or footer.

Why it matters: Layout routes let you share a persistent shell (header, sidebar, footer) across multiple pages without duplicating markup.

Real applications: App shell with navigation for logged-in pages, dashboard layout with a sidebar shared across all dashboard routes.

Common mistakes: Forgetting to add <Outlet /> in the layout component — the child route content will never appear.

React Router provides two redirect mechanisms. The <Navigate> component is declarative — when rendered it immediately redirects, ideal for conditional JSX redirects like protected route checks. The navigate() function from useNavigate is imperative — called inside event handlers or after async operations. Pass { replace: true } to both to replace the current history entry instead of adding one, preventing users from back-buttoning into the page they were redirected away from.
import { Navigate, useNavigate } from 'react-router-dom';

// Declarative redirect — e.g., redirect /home to /
<Route path="/home" element={<Navigate to="/" replace />} />

// Programmatic — e.g., after form submission
function LoginForm() {
  const navigate = useNavigate();
  const handleSubmit = async (e) => {
    e.preventDefault();
    await login(credentials);
    navigate('/dashboard', { replace: true });
  };
}
Use replace: true so the user cannot click Back to return to the login page after a successful login.

Why it matters: Redirects guide users to the right page after authentication or when accessing a deprecated URL.

Real applications: Redirecting from /home to /, sending users to a dashboard after login, redirecting old URLs to new ones.

Common mistakes: Using navigate without replace: true after login — pressing Back takes the user back to the login page instead of before it.

BrowserRouter uses the HTML5 History API for clean URLs like /about, but requires your web server to serve index.html for all paths — otherwise refreshing on a deep route returns a server 404. HashRouter uses the URL hash fragment (/#/about) — since the hash is never sent to the server, it works on any static host (GitHub Pages, S3) without special server configuration. Use BrowserRouter for production apps with a configurable server, and HashRouter for simple static deployments.
// BrowserRouter — clean URLs, requires server config for deep links
import { BrowserRouter } from 'react-router-dom';
<BrowserRouter><App /></BrowserRouter>

// HashRouter — hash-based URLs, works on GitHub Pages, S3 static hosting
import { HashRouter } from 'react-router-dom';
<HashRouter><App /></HashRouter>
Use BrowserRouter for production apps with server support. Use HashRouter for static hosting where you cannot configure the server to serve index.html for all routes.

Why it matters: The wrong router causes 404 errors when users refresh or directly visit a URL in their browser.

Real applications: BrowserRouter for apps on Vercel or Netlify with redirects configured; HashRouter for apps hosted on plain S3 or GitHub Pages.

Common mistakes: Using BrowserRouter on a static host without configuring the server to serve index.html for all routes — refreshing any page returns a 404.