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
:idsyntax - 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:
<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:
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/listing/:id" element={<ListingDetailsPage />} />
</Routes>What :id means:
:tells React Router "this part is variable"idis 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:
// [!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:
- Imported ListingDetailsPage - Need to import before using
- Added dynamic route -
/listing/:idmatches any listing URL - 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/999What you should see:
- The same
ListingDetailsPagecomponent 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:
listingmatcheslisting - Segment 2:
:idmatches anything (42)
- Segment 1:
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:
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/newis 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/newmatches/listing/:idfirst :idbecomes "new"CreateListingPagenever renders!- User sees listing details for ID "new" (probably an error)
React Router uses "first match wins" strategy:
- Check routes in order (top to bottom)
- First route that matches the URL wins
- Render that route's element
- 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/42Lesson 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/testAll 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-pageYou 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
:idparameter - ✅ 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) :idsyntax - 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! 🎯