Most MERN Blog pages are tied to a specific resource โ a post identified by an ID in the URL, a user profile identified by a username, a tag page identified by a slug. These are dynamic routes: the URL path contains a variable segment that changes per resource. React Router’s URL parameter syntax (:id, :slug) lets you define one route that handles all variations, and the useParams hook extracts the variable value so you can fetch the right resource from the Express API. In this lesson you will build the MERN Blog’s dynamic post detail route, a slug-based lookup, and the nested route pattern for the dashboard layout.
Defining Dynamic Routes
// In App.jsx โ :id is a URL parameter placeholder
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/posts/:id" element={<PostDetailPage />} /> {/* :id is dynamic */}
<Route path="/users/:username" element={<UserProfilePage />} />
{/* Multiple params in one path */}
<Route path="/posts/:postId/comments/:commentId" element={<CommentPage />} />
{/* Catch-all โ must be last */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
useParams() returns { id: '64a1f2b3c8e4d5f6a7b8c9d0' } as a string, not an ObjectId. For MongoDB queries via Mongoose this is fine โ Mongoose automatically converts a valid 24-character hex string to an ObjectId. For other numeric IDs you would need to parse: parseInt(id, 10)._id in the URL (/posts/:id) rather than a slug (/posts/:slug) if you also want slug-based URLs. Store both on the post model and define both routes: /posts/:id for API-linked navigation (where you have the ID) and /blog/:slug for SEO-friendly sharing URLs. Express can handle both at the API level./posts/new (specific) and /posts/:id (dynamic) both work correctly because React Router v6 ranks routes by specificity. However, if you have both /posts/new and /posts/:id, always define the more specific route first to make your intent explicit.useParams โ Reading URL Parameters
import { useParams } from 'react-router-dom';
import { useState, useEffect } from 'react';
import axios from 'axios';
function PostDetailPage() {
const { id } = useParams(); // reads ":id" from the URL
// URL: /posts/64a1f2b3c8e4d5f6a7b8c9d0 โ id = '64a1f2b3c8e4d5f6a7b8c9d0'
const [post, setPost] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!id) return;
const controller = new AbortController();
const fetchPost = async () => {
try {
setPost(null);
setLoading(true);
setError(null);
const res = await axios.get(`/api/posts/${id}`, { signal: controller.signal });
setPost(res.data.data);
} catch (err) {
if (!axios.isCancel(err)) {
setError(
err.response?.status === 404 ? 'Post not found' : 'Failed to load post'
);
}
} finally {
setLoading(false);
}
};
fetchPost();
return () => controller.abort();
}, [id]); // re-fetch when id changes (user navigates between posts)
if (loading) return <Spinner />;
if (error) return <ErrorMessage message={error} />;
if (!post) return null;
return (
<article className="post-detail">
<h1>{post.title}</h1>
<div className="post-detail__meta">
<Avatar src={post.author?.avatar} name={post.author?.name || 'Unknown'} size={32} />
<span>{post.author?.name}</span>
<time>{new Date(post.createdAt).toLocaleDateString()}</time>
</div>
<div className="post-detail__body"
dangerouslySetInnerHTML={{ __html: post.body }} />
</article>
);
}
useSearchParams โ URL Query String
import { useSearchParams } from 'react-router-dom';
// Read and update URL query string parameters
// URL: /posts?tag=mern&page=2
function BlogPage() {
const [searchParams, setSearchParams] = useSearchParams();
// Read values
const tag = searchParams.get('tag') || '';
const page = parseInt(searchParams.get('page') || '1', 10);
// Update โ changes URL without reload
const setTag = (newTag) => {
setSearchParams(prev => {
const next = new URLSearchParams(prev);
if (newTag) next.set('tag', newTag);
else next.delete('tag');
next.set('page', '1'); // reset page when tag changes
return next;
});
};
// Filter buttons update the URL โ users can bookmark filtered views
return (
<div>
<button onClick={() => setTag('mern')}>#mern</button>
<button onClick={() => setTag('react')}>#react</button>
<button onClick={() => setTag('')}>All Posts</button>
<PostList tag={tag} page={page} />
</div>
);
}
useLocation โ Current URL Information
import { useLocation } from 'react-router-dom';
// Read the full current location object
function AnalyticsTracker() {
const location = useLocation();
useEffect(() => {
// Track page view on every route change
analytics.pageView(location.pathname);
}, [location.pathname]);
return null;
}
// location object:
// {
// pathname: '/posts/64a1f2b3...',
// search: '?tag=mern',
// hash: '#comments',
// state: { from: '/login' }, // state passed via navigate()
// key: 'default'
// }
Common Mistakes
Mistake 1 โ Not including the param in the useEffect dependency array
โ Wrong โ effect does not re-run when the ID changes:
const { id } = useParams();
useEffect(() => {
fetchPost(id); // id from first render only
}, []); // missing id โ stale data when navigating between posts
โ Correct:
useEffect(() => { fetchPost(id); }, [id]); // โ re-fetches on id change
Mistake 2 โ Trying to parse a MongoDB ObjectId as a number
โ Wrong โ ObjectId is a 24-char hex string, not a number:
const id = parseInt(useParams().id, 10); // NaN โ ObjectId is not a number
โ Correct โ use the string directly with Mongoose:
const { id } = useParams(); // '64a1f2b3...' as string โ Mongoose handles conversion โ
Mistake 3 โ Not clearing previous post state when navigating between posts
โ Wrong โ old post visible while new one loads:
useEffect(() => {
fetchPost(id).then(setPost); // old post shows until new data arrives
}, [id]);
โ Correct โ clear post immediately when ID changes:
useEffect(() => {
setPost(null); // clear immediately โ spinner shows
setLoading(true);
fetchPost(id).then(setPost).finally(() => setLoading(false));
}, [id]); // โ
Quick Reference
| Task | Code |
|---|---|
| Define dynamic route | <Route path="/posts/:id" element={<Detail />} /> |
| Read URL param | const { id } = useParams() |
| Read query string | const [params, setParams] = useSearchParams() |
| Get query value | params.get('tag') |
| Set query value | setSearchParams({ tag: 'mern', page: '1' }) |
| Current pathname | useLocation().pathname |
| Location state | useLocation().state |