Code To Learn logo

Code To Learn

M4: Routes & Navigation

L5: Adding Dynamic Route Parameter

Configure routes with URL parameters to handle multiple listings

Now that we have a ListingDetailsPage component, we need to connect it to a route. But here's the challenge: we have many listings (listing 1, listing 2, listing 3, etc.), and we need one route that works for all of them!

This is where dynamic routes come in - routes with variable parts that change based on the URL.

What You'll Learn

  • What dynamic routes are and why we need them
  • How to add URL parameters to routes
  • Understanding the :id syntax
  • How React Router matches dynamic routes
  • Preparing for data fetching based on URL

The Problem: One Page, Many Listings

Right now, we could create separate routes for each listing:

❌ Bad Approach - Don't Do This
<Routes>
  <Route path="/" element={<HomePage />} />
  <Route path="/listing-1" element={<ListingDetailsPage />} />
  <Route path="/listing-2" element={<ListingDetailsPage />} />
  <Route path="/listing-3" element={<ListingDetailsPage />} />
  {/* What about listing 100? Or 1000? */}
</Routes>

Problems with this approach:

  • ❌ Need to manually add a route for every listing
  • ❌ Can't add new listings dynamically
  • ❌ Lots of repetitive code
  • ❌ Doesn't scale at all

The Solution: Dynamic Routes

Instead, we create one route that handles all listings:

✅ Good Approach - Dynamic Route
<Routes>
  <Route path="/" element={<HomePage />} />
  <Route path="/listing/:id" element={<ListingDetailsPage />} />
</Routes>

What :id means:

  • : tells React Router "this part is variable"
  • id is the parameter name (you choose this)
  • Works for /listing/1, /listing/2, /listing/999, etc.

Dynamic Routes Rock!

With one dynamic route, you can handle unlimited listings! The :id part acts as a placeholder that matches any value in that position of the URL.

Understanding Dynamic Segments

Let's break down the route path:

/listing/ - The static part

This part never changes. It's always literally "listing".

Examples:

  • /listing/1 - Matches
  • /listing/abc - Matches
  • /property/1 - Doesn't match

The static part helps organize your URLs and makes them readable.

:id - The dynamic part

This part can be anything! The : makes it a parameter.

Examples:

  • /listing/1 → id = "1"
  • /listing/42 → id = "42"
  • /listing/abc-123 → id = "abc-123"

You can name it anything:

  • :id (common for IDs)
  • :listingId (more specific)
  • :slug (for text-based URLs)

/listing/:id - Complete route

Combines static and dynamic parts.

What matches:

  • /listing/1
  • /listing/999
  • /listing/beach-house

What doesn't match:

  • /listing (missing ID)
  • /listing/1/details (extra segment)
  • /listings/1 (wrong static part)

The route must match exactly, except for the dynamic segment!

Step 1: Add the Dynamic Route

Let's update the Router component to include our dynamic route:

Update Router.jsx

Open Router.jsx and add the new route:

src/components/Router.jsx
// [!code word:/listing/:id]
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import HomePage from '../pages/HomePage';
import ListingDetailsPage from '../pages/ListingDetailsPage'; 

export default function Router() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/listing/:id" element={<ListingDetailsPage />} /> // [!code ++]
      </Routes>
    </BrowserRouter>
  );
}

What changed:

  1. Imported ListingDetailsPage - Need to import before using
  2. Added dynamic route - /listing/:id matches any listing URL
  3. Order matters - More specific routes should come before general ones

Test the Route

Open your browser and manually type these URLs:

Try these in your browser:

http://localhost:5173/listing/1
http://localhost:5173/listing/2
http://localhost:5173/listing/999

What you should see:

  • The same ListingDetailsPage component renders for all URLs
  • The page shows the same content (we haven't used the ID yet)
  • No 404 error or blank page

Why it's the same content:

Right now, the page shows hardcoded data. In Lesson 6, we'll extract the id from the URL and use it to fetch the correct listing!

How Route Matching Works

React Router follows this process when you navigate to a URL:

Step 1: Parse the URL

User navigates to /listing/42

React Router breaks it down:

  • Segment 1: listing
  • Segment 2: 42

Step 2: Check Each Route

React Router compares the URL against each route in order:

Route 1: path="/"

  • Segments: [] (empty, just root)
  • Match? ❌ No (different number of segments)

Route 2: path="/listing/:id"

  • Segments: listing, :id (variable)
  • Match? ✅ Yes!
    • Segment 1: listing matches listing
    • Segment 2: :id matches anything (42)

Step 3: Render the Element

Match found! Render the ListingDetailsPage component.

React Router also stores the parameter:

  • Parameter name: id
  • Parameter value: "42"

In Lesson 6, we'll learn how to access this stored value!

Multiple Dynamic Parameters

You can have multiple dynamic segments in one route:

Route: /listing/:id

Example URLs:

  • /listing/1 → id = "1"
  • /listing/42 → id = "42"

Use case: Single resource by ID

<Route path="/listing/:id" element={<ListingDetailsPage />} />

Route: /listing/:id/review/:reviewId

Example URLs:

  • /listing/1/review/5 → id = "1", reviewId = "5"
  • /listing/42/review/99 → id = "42", reviewId = "99"

Use case: Nested resources

<Route 
  path="/listing/:id/review/:reviewId" 
  element={<ReviewPage />} 
/>

Route: /user/:userId/listing/:listingId/booking/:bookingId

Example URL:

  • /user/10/listing/42/booking/123
    • userId = "10"
    • listingId = "42"
    • bookingId = "123"

Use case: Deeply nested resources

<Route 
  path="/user/:userId/listing/:listingId/booking/:bookingId" 
  element={<BookingDetailsPage />} 
/>

Nested Routes

For our StaySense app, we only need one parameter (:id). But knowing you can use multiple parameters helps when building more complex applications!

Complete Router Code

Here's what your Router component should look like now:

src/components/Router.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import HomePage from '../pages/HomePage';
import ListingDetailsPage from '../pages/ListingDetailsPage';

export default function Router() {
  return (
    <BrowserRouter>
      <Routes>
        {/* Static route - exact match */}
        <Route path="/" element={<HomePage />} />
        
        {/* Dynamic route - matches any ID */}
        <Route path="/listing/:id" element={<ListingDetailsPage />} />
      </Routes>
    </BrowserRouter>
  );
}

Route Order and Priority

The order of routes matters when you have overlapping patterns:

✅ Specific routes before general routes:

<Routes>
  <Route path="/" element={<HomePage />} />
  <Route path="/listing/new" element={<CreateListingPage />} />
  <Route path="/listing/:id" element={<ListingDetailsPage />} />
  <Route path="*" element={<NotFoundPage />} />
</Routes>

Why this works:

  • /listing/new is checked before /listing/:id
  • If URL is /listing/new, it matches the specific route
  • If URL is /listing/42, it matches the dynamic route
  • * (catch-all) is last, so it only matches if nothing else does

❌ General routes before specific routes:

<Routes>
  <Route path="/" element={<HomePage />} />
  <Route path="/listing/:id" element={<ListingDetailsPage />} />
  <Route path="/listing/new" element={<CreateListingPage />} />
  <Route path="*" element={<NotFoundPage />} />
</Routes>

Problem:

  • URL /listing/new matches /listing/:id first
  • :id becomes "new"
  • CreateListingPage never renders!
  • User sees listing details for ID "new" (probably an error)

React Router uses "first match wins" strategy:

  1. Check routes in order (top to bottom)
  2. First route that matches the URL wins
  3. Render that route's element
  4. Stop checking remaining routes

Best practice:

Most Specific

Less Specific

Catch-All (*)

Example:

// 1. Exact matches
<Route path="/about" element={<AboutPage />} />

// 2. Specific dynamic routes
<Route path="/listing/new" element={<CreateListingPage />} />

// 3. General dynamic routes
<Route path="/listing/:id" element={<ListingDetailsPage />} />

// 4. Catch-all
<Route path="*" element={<NotFoundPage />} />

Common URL Parameter Patterns

What Happens Next?

Right now, our dynamic route works, but the page doesn't use the ID from the URL. Here's what we'll do in the next lessons:

Lesson 6: Extract the ID

Use the useParams hook to get the id from the URL:

const { id } = useParams(); // id = "42" from /listing/42

Lesson 7: Fetch Data

Use the ID to fetch the correct listing from the API:

useEffect(() => {
  fetch(`/api/listings/${id}`).then(/* ... */);
}, [id]);

Lesson 8: Display Data

Create a component to show the fetched listing details beautifully!

Testing Your Routes

Here's how to verify your dynamic route is working:

Test Different IDs

Manually type these URLs in your browser:

http://localhost:5173/listing/1
http://localhost:5173/listing/2
http://localhost:5173/listing/99
http://localhost:5173/listing/test

All should render the ListingDetailsPage (with same hardcoded content).

Check the Browser Console

Open DevTools (F12) and check for errors. You should see no errors.

Try Invalid Routes

Try a URL that doesn't match any route:

http://localhost:5173/invalid-page

You should see a blank page (we'll fix this with a 404 page in Lesson 10).

Common Questions

Dynamic Route Added!

Your Router now supports unlimited listings with one dynamic route! In the next lesson, we'll learn how to extract and use the ID from the URL with the useParams hook.

Quick Recap

What we accomplished:

  • ✅ Added dynamic route with :id parameter
  • ✅ Imported and used ListingDetailsPage component
  • ✅ Tested URLs with different IDs
  • ✅ Understood route matching and order
  • ✅ Learned about multiple parameters

Key concepts:

  • Dynamic routes - Routes with variable segments (:paramName)
  • :id syntax - Defines a URL parameter
  • Route matching - First match wins strategy
  • Route order - Specific before general
  • Scalability - One route handles unlimited resources

What's Next?

In Lesson 6, we'll use the useParams hook to extract the id from the URL. This is how we'll know which listing to display - the ID tells us exactly what data to fetch! 🎯