Hassan Agmir Hassan Agmir

Add React Router To Existing Project

Hassan Agmir
Add React Router To Existing Project

Introduction to React Router

Single-Page Applications (SPAs) have revolutionized front-end development by providing smooth, app-like experiences in the browser. However, as your React project grows, you’ll need a robust way to handle multiple “pages” or views—enter React Router.

This article will guide you through integrating React Router into an existing React project. You’ll learn:

  • Why you need a routing library
  • How to install and configure React Router
  • Defining routes, nested routes, and dynamic parameters
  • Navigation components (<Link>, <NavLink>, programmatic navigation)
  • Advanced patterns: code splitting, route guards, 404 pages
  • Migrating from React Router v5 to v6
  • Best practices, performance tips, and troubleshooting

By the end, your app will seamlessly handle multiple views, improve user experience, and be easier to maintain.

Why Use React Router?

Before diving in, let’s quickly recap why a dedicated routing library like React Router is essential:

  1. Client-Side URL Management
    Manipulate the browser’s address bar without full page reloads.
  2. Nested Views
    Render different UI components based on the URL, supporting nested layouts.
  3. Declarative Routing
    Define your routes in JSX, keeping routing logic close to UI.
  4. Dynamic Parameters
    Capture URL segments (e.g., /products/:id) and access them in components.
  5. Navigation Components
    Easy <Link> and <NavLink> for accessible, SEO-friendly navigation.
  6. Advanced Features
    Redirects, route guards, code splitting, scroll restoration, etc.

If your project currently uses conditional rendering based on state or window.location, integrating React Router will dramatically simplify your code and make it more scalable.

Prerequisites

  • Node.js (v14+) and npm or Yarn installed
  • An existing React project created via create-react-app, Vite, Next.js (client-side only), or custom setup
  • Basic familiarity with React components, hooks, and JSX
Note: This guide assumes you’re using React 18+ and targeting browsers that support the HTML5 history API (modern browsers). For legacy support, you can polyfill or use the hash-based router.

1. Installing React Router

React Router v6 is the latest major version (as of May 2025). Install the core packages:

# Using npm
npm install react-router-dom@6

# Or using Yarn
yarn add react-router-dom@6

This adds the following packages:

  • react-router: core routing engine
  • react-router-dom: browser-specific bindings

2. Setting Up the Router

Wrap your application in a router provider, typically in src/index.js or src/main.jsx:

// src/index.js (Create React App)
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);
Why BrowserRouter?
It uses the HTML5 history API to keep your UI in sync with the URL. Alternative routers include HashRouter (for non-HTML5 environments) and MemoryRouter (for testing).

3. Defining Routes

Inside your main App component, import routing primitives and define your route hierarchy:

// src/App.jsx
import { Routes, Route, Link } from 'react-router-dom';
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
import ProductsPage from './pages/ProductsPage';
import ProductDetail from './pages/ProductDetail';
import NotFound from './pages/NotFound';

function App() {
  return (
    <div>
      <nav>
        <Link to="/">Home</Link> |{' '}
        <Link to="/about">About</Link> |{' '}
        <Link to="/products">Products</Link>
      </nav>

      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="about" element={<AboutPage />} />
        <Route path="products" element={<ProductsPage />} />

        {/* Dynamic route for product details */}
        <Route path="products/:productId" element={<ProductDetail />} />

        {/* 404 Not Found route */}
        <Route path="*" element={<NotFound />} />
      </Routes>
    </div>
  );
}

export default App;
  • <Routes> replaces v5’s <Switch>.
  • The path prop defines the URL segment.
  • The element prop renders the component.
  • Nesting <Route> under another <Route> supports nested routes.

4. Creating Page Components

Create simple placeholders for each page in src/pages/:

// src/pages/HomePage.jsx
export default function HomePage() {
  return <h1>Welcome to Our Store</h1>;
}

// src/pages/AboutPage.jsx
export default function AboutPage() {
  return <h1>About Us</h1>;
}

// src/pages/ProductsPage.jsx
import { Link } from 'react-router-dom';

const fakeProducts = [
  { id: 1, name: 'Widget' },
  { id: 2, name: 'Gadget' },
  // …
];

export default function ProductsPage() {
  return (
    <>
      <h1>Products</h1>
      <ul>
        {fakeProducts.map(p => (
          <li key={p.id}>
            <Link to={`/products/${p.id}`}>{p.name}</Link>
          </li>
        ))}
      </ul>
    </>
  );
}

// src/pages/ProductDetail.jsx
import { useParams, useNavigate } from 'react-router-dom';

export default function ProductDetail() {
  const { productId } = useParams();
  const navigate = useNavigate();

  // Simulate fetching product by ID
  const product = { id: productId, name: `Product ${productId}`, desc: 'Delicious product.' };

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.desc}</p>
      <button onClick={() => navigate(-1)}>Go Back</button>
    </div>
  );
}

// src/pages/NotFound.jsx
export default function NotFound() {
  return <h1>404 — Page Not Found</h1>;
}

Key hooks:

  • useParams(): Access dynamic segments.
  • useNavigate(): Imperative navigation (e.g., redirect, go back).

5. Nested Routes and Layouts

Often you want common layouts (e.g., sidebar, header) with nested content. React Router v6 lets you nest routes directly:

// src/App.jsx
import { Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import Dashboard from './pages/Dashboard';
import Reports from './pages/Reports';
import Settings from './pages/Settings';

function App() {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Dashboard />} />
        <Route path="reports" element={<Reports />} />
        <Route path="settings" element={<Settings />} />
        <Route path="*" element={<NotFound />} />
      </Route>
    </Routes>
  );
}

// src/components/Layout.jsx import { Outlet, NavLink } from 'react-router-dom'; export default function Layout() { return ( <div> <nav> <NavLink to="/">Dashboard</NavLink> <NavLink to="reports">Reports</NavLink> <NavLink to="settings">Settings</NavLink> </nav> <main> <Outlet /> {/* Renders the matched child route */} </main> </div> ); }
  • <Outlet> is the placeholder for nested content.
  • Use <NavLink> for active link styling.

6. Styling Active Links

With <NavLink>, you can apply styles/classes based on active state:

<NavLink
  to="/reports"
  className={({ isActive }) => (isActive ? 'active-link' : '')}
>
  Reports
</NavLink>

/* src/index.css */ .active-link { font-weight: bold; color: teal; }

7. Redirects and Index Routes

To redirect or set default child routes:

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

<Routes>
  <Route path="/" element={<Layout />}>
    {/* Default redirect to dashboard */}
    <Route index element={<Navigate to="dashboard" replace />} />

    <Route path="dashboard" element={<Dashboard />} />
    {/* … */}
  </Route>
</Routes>
  • <Navigate> programmatically redirects.
  • replace prevents adding an extra entry in history.

8. Route Guards (Protected Routes)

Implement authentication checks:

// src/components/RequireAuth.jsx
import { Navigate, useLocation } from 'react-router-dom';

export default function RequireAuth({ children }) {
  const isAuthenticated = Boolean(localStorage.getItem('token'));
  const location = useLocation();

  if (!isAuthenticated) {
    // Redirect to login, preserving the intended path
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  return children;
}

// Usage in routes:
<Routes>
  <Route path="/" element={<Layout />}>
    <Route index element={<HomePage />} />
    <Route
      path="dashboard"
      element={
        <RequireAuth>
          <Dashboard />
        </RequireAuth>
      }
    />
    {/* … */}
  </Route>
</Routes>

9. Code Splitting with React.lazy

Improve performance by lazily loading route components:

import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Reports = lazy(() => import('./pages/Reports'));

function App() {
  return (
    <Suspense fallback={<div>Loading…</div>}>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Dashboard />} />
          <Route path="reports" element={<Reports />} />
        </Route>
      </Routes>
    </Suspense>
  );
}

10. Query Parameters and URLSearchParams

Handle query strings:

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

function SearchResults() {
  const { search } = useLocation();
  const params = new URLSearchParams(search);
  const term = params.get('q') || '';

  // Fetch results based on `term`…
  return <div>Showing results for "{term}"</div>;
}

Link to /search?q=react-router.

11. Programmatic Navigation

Aside from <Link>, you can navigate in code:

const navigate = useNavigate();

// Navigate to home
navigate('/');

// Go back
navigate(-1);

// Replace history
navigate('/login', { replace: true });

12. Handling 404s and Fallbacks

Add a catch-all route:

<Route path="*" element={<NotFound />} />

Place at the end of your <Routes> so unmatched URLs render your 404 page.

13. Migrating from React Router v5 to v6

If you already have v5 in your project, here are key changes:

  • Route switch
    • v5: <Switch>
    • v6: <Routes>
  • Route definitions
    • v5: <Route path="/" component={Home} />
    • v6: <Route path="/" element={<Home />} />
  • Nested routes
    • v5: Manual prefixing children
    • v6: In-tree nesting under parent <Route>
  • Redirects
    • v5: <Redirect to="/" />
    • v6: <Navigate to="/" replace />
  • useHistory
    • v5: const history = useHistory()
    • v6: const navigate = useNavigate()
  • Active links
    • v5: NavLink activeClassName
    • v6: NavLink className={({ isActive }) => …}

Refactor accordingly to enjoy simpler, more declarative routing.

14. Best Practices

  1. Keep Routes Flat When Possible
    Deep nesting can become confusing; use layouts to avoid repetition.
  2. Co-locate Routes
    Keep routes close to related components for easier maintenance.
  3. Use Index Routes
    Eliminate empty paths by specifying index for default child routes.
  4. Debounce Navigation
    Prevent rapid spamming of navigate() for performance.
  5. Error Boundaries
    Wrap lazy-loaded routes in error boundaries to catch load failures.
  6. Accessibility
    Ensure your <nav> uses semantic elements and <Link> over <a> for client-side routing.

15. Troubleshooting

  • Blank Page on Refresh
    Ensure your server serves index.html for all routes (history API fallback).
  • 404 on Deep Link
    Configure your hosting (e.g., Netlify, Vercel, Apache) to redirect all paths to your entry point.
  • Query Params Not Updating
    Use useLocation() + useEffect to watch changes in location.search.
  • TypeScript Issues
    Install types via npm install --save-dev @types/react-router-dom, and ensure proper imports.

16. Scroll Restoration

By default, when navigating between routes, the browser tries to preserve the scroll position. In SPAs, this may lead to unexpected behavior—for example, navigating back to a long list leaves you scrolled to the bottom. React Router v6 provides a <ScrollRestoration> component to manage this:

// src/components/ScrollToTop.jsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

export default function ScrollToTop() {
  const { pathname } = useLocation();
  useEffect(() => {
    window.scrollTo(0, 0);
  }, [pathname]);
  return null;
}

Add it inside your router:

<BrowserRouter>
  <ScrollToTop />
  <App />
</BrowserRouter>

This simple component scrolls the window to the top on every path change. For more fine-grained control, you can store positions in a ref or context and restore them when navigating back.

17. Route-Based Code Splitting with Loaders

React Router v6.4+ introduces data routers with createBrowserRouter, RouterProvider, and loaders—enabling both code and data to load before rendering:

npm install react-router-dom@6.4

// src/router.js import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import RootLayout from './layouts/RootLayout'; import HomePage, { loader as homeLoader } from './pages/HomePage'; import ProductsPage, { loader as productsLoader } from './pages/ProductsPage'; import ProductDetail, { loader as detailLoader } from './pages/ProductDetail'; import NotFound from './pages/NotFound'; const router = createBrowserRouter([ { path: '/', element: <RootLayout />, children: [ { index: true, element: <HomePage />, loader: homeLoader }, { path: 'products', element: <ProductsPage />, loader: productsLoader }, { path: 'products/:productId', element: <ProductDetail />, loader: detailLoader, }, { path: '*', element: <NotFound /> }, ], }, ]); export default function App() { return <RouterProvider router={router} />; }

Each page exports a loader function:

// src/pages/ProductsPage.jsx
export async function loader() {
  const resp = await fetch('/api/products');
  if (!resp.ok) throw new Error('Failed to load products');
  return resp.json();
}

export default function ProductsPage() {
  const products = useLoaderData();
  // ...
}

This approach pre-fetches data and code before the route renders, avoiding loading spinners within the component and ensuring data is available immediately.

18. Route Transitions and Animations

To enhance user experience, you can animate between route changes using libraries like Framer Motion:

npm install framer-motion

// src/components/AnimatedRoutes.jsx import { AnimatePresence, motion } from 'framer-motion'; import { useLocation, Routes, Route } from 'react-router-dom'; import HomePage from '../pages/HomePage'; import AboutPage from '../pages/AboutPage'; // ... export default function AnimatedRoutes() { const location = useLocation(); return ( <AnimatePresence exitBeforeEnter> <Routes key={location.pathname} location={location}> <Route path="/" element={ <motion.div initial={{ opacity: 0, x: -50 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: 50 }} > <HomePage /> </motion.div> } /> <Route path="/about" element={ <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}> <AboutPage /> </motion.div> } /> {/* ...other routes */} </Routes> </AnimatePresence> ); }

Wrap your <AnimatedRoutes /> inside your router provider. This gives smooth fade/slide effects during navigation.

19. Handling Internationalization (i18n)

For multi-language support, integrate a library like react-i18next with React Router:

npm install react-i18next i18next

// src/i18n.js import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import en from './locales/en.json'; import fr from './locales/fr.json'; i18n.use(initReactI18next).init({ resources: { en: { translation: en }, fr: { translation: fr } }, lng: 'en', fallbackLng: 'en', interpolation: { escapeValue: false }, }); export default i18n;

Wrap your app:

import './i18n';
import { I18nextProvider } from 'react-i18next';

<BrowserRouter>
  <I18nextProvider i18n={i18n}>
    <App />
  </I18nextProvider>
</BrowserRouter>;

Then you can create routes for different locales:

<Routes>
  <Route path=":lng" element={<Layout />}>
    <Route index element={<HomePage />} />
    {/* ... */}
  </Route>
  <Route path="*" element={<Navigate to="/en" replace />} />
</Routes>

In your Layout component, sync the URL param with i18n:

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

export default function Layout() {
  const { lng } = useParams();
  useEffect(() => {
    if (lng && i18n.language !== lng) i18n.changeLanguage(lng);
  }, [lng]);
  // ...
}

20. SEO Considerations

Although client-side routing can hinder SEO, you can mitigate by:

  • Server-Side Rendering (SSR)
    Use frameworks like Next.js or Remix that support SSR out of the box.
  • Pre-rendering / Static Site Generation (SSG)
    Generate HTML at build time (e.g., Gatsby, Vite with prerender plugins).
  • Meta Tags Management
    Dynamically set page titles and meta descriptions using react-helmet-async:
  • npm install react-helmet-async
    
  • import { Helmet, HelmetProvider } from 'react-helmet-async';
    
    export default function ProductDetail() {
      const product = useLoaderData();
      return (
        <>
          <Helmet>
            <title>{product.name} | My Store</title>
            <meta name="description" content={product.shortDesc} />
          </Helmet>
          {/* ... */}
        </>
      );
    }

Wrap your app in <HelmetProvider> to ensure proper context.

21. Testing Your Routes

Use React Testing Library and Jest to verify routing behavior:

npm install --save-dev @testing-library/react @testing-library/jest-dom jest

// src/__tests__/App.test.jsx import { render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import App from '../App'; test('renders home page by default', () => { render( <MemoryRouter initialEntries={['/']}> <App /> </MemoryRouter> ); expect(screen.getByText(/Welcome to Our Store/i)).toBeInTheDocument(); }); test('renders 404 on unknown route', () => { render( <MemoryRouter initialEntries={['/random']}> <App /> </MemoryRouter> ); expect(screen.getByText(/404 — Page Not Found/i)).toBeInTheDocument(); });
  • Use MemoryRouter for isolated route tests.
  • Test dynamic params:
  • test('renders product detail', () => {
      render(
        <MemoryRouter initialEntries={['/products/42']}>
          <App />
        </MemoryRouter>
      );
      expect(screen.getByText(/Product 42/i)).toBeInTheDocument();
    });

22. Server Configuration for Client-Side Routing

When deploying SPAs, you must configure your server or CDN to redirect all non-static-file requests to index.html:

  • Netlify
    Create a _redirects file:
  • /*  /index.html  200
  • Vercel
    Automatic with vercel.json:
  • {
      "rewrites": [{ "source": "/(.*)", "destination": "/" }]
    }
  • Apache
  • <IfModule mod_rewrite.c>
      RewriteEngine On
      RewriteBase /
      RewriteRule ^index\.html$ – [L]
      RewriteCond %{REQUEST_FILENAME} !-f
      RewriteCond %{REQUEST_FILENAME} !-d
      RewriteRule . /index.html [L]
    </IfModule>
  • NGINX
  • location / {
      try_files $uri /index.html;
    }

23. Migrating to Next.js or Remix

If you outgrow CRA and need server-side rendering or more built-in routing features, consider:

  • Next.js
    File-based routing under pages/:
  • pages/
    ├ index.jsx      // '/'
    ├ about.jsx      // '/about'
    └ products/
      ├ index.jsx    // '/products'
      └ [id].jsx     // '/products/:id'
  • Next.js handles SSR, SSG, API routes, and image optimization.
  • Remix
    Nested routes in app/routes/ with loaders and actions for full-stack data loading.

Both frameworks integrate smoothly with React Router’s concepts but offer more conventions and tooling for scale.

24. Performance Optimization

  • Prefetching Links
    Use <Link prefetch="intent"> (in future React Router tiers) or manually trigger data fetch on hover.
  • Minimize Bundle Size
    Analyze with Webpack Bundle Analyzer and split large dependencies.
  • Throttle Rapid Navigation
    Debounce navigate() calls in response to rapid UI events.
  • Use Pure Components & Memoization
    Prevent unnecessary re-renders when routing.

25. Common Pitfalls & Debugging Tips

  • Blank page on first load
    • Symptom: White screen, no errors in console
    • Fix: Check router provider and ensure correct root.render wrapper.
  • Unexpected URL reloads
    • Symptom: Full page refresh on <Link> click
    • Fix: Ensure you’re using Link from react-router-dom, not <a>.
  • Route not matching dynamic segment
    • Symptom: Wrong props in component
    • Fix: Verify the path parameter and destructure useParams() correctly.
  • Missing trailing slashes
    • Symptom: Redirect loops
    • Fix: Harmonize trailing slash behavior or strip slashes in paths.
  • 404 on refresh in production
    • Symptom: Server returns 404
    • Fix: Configure history API fallback in hosting setup.

Conclusion

Congratulations! You’ve now explored both fundamental and advanced aspects of adding React Router to an existing project:

  1. Basic setup and route definitions
  2. Nested layouts, dynamic parameters, and navigation hooks
  3. Scroll restoration, data loaders, and code splitting
  4. Animations, i18n, and SEO optimizations
  5. Testing, server config, and migration paths
  6. Performance tuning and debugging

With these patterns and best practices, your React application will feature robust, maintainable routing and an exceptional user experience. As your application evolves, continue to revisit these techniques—refactoring routes, fine-tuning data loaders, and embracing modern features in React Router and complementary frameworks.

Subscribe to my Newsletters

Stay updated with the latest programming tips, tricks, and IT insights! Join my community to receive exclusive content on coding best practices.

© Copyright 2025 by Hassan Agmir . Built with ❤ by Me