Route Parameters
Route Parameters in React Router
Route parameters allow you to create dynamic routes that can handle variable segments in the URL. This is essential for building applications with dynamic content like user profiles, product pages, or blog posts.
Basic Route Parameters
Single Parameter
import { Routes, Route, useParams } from 'react-router-dom';
// Define route with parameter
function App() {
return (
<Routes>
<Route path="/users/:userId" element={<UserProfile />} />
<Route path="/posts/:postId" element={<BlogPost />} />
</Routes>
);
}
// Access parameter in component
function UserProfile() {
const { userId } = useParams();
return (
<div>
<h1>User Profile</h1>
<p>User ID: {userId}</p>
</div>
);
}
Multiple Parameters
// Route with multiple parameters
<Route path="/posts/:year/:month/:slug" element={<BlogPost />} />
// Access multiple parameters
function BlogPost() {
const { year, month, slug } = useParams();
return (
<div>
<h1>Blog Post</h1>
<p>Date: {year}/{month}</p>
<p>Slug: {slug}</p>
</div>
);
}
Optional Parameters
Using Optional Segments
// Optional parameter with ?
<Route path="/products/:category/:subcategory?" element={<Products />} />
function Products() {
const { category, subcategory } = useParams();
return (
<div>
<h1>Products</h1>
<p>Category: {category}</p>
{subcategory && <p>Subcategory: {subcategory}</p>}
</div>
);
}
Multiple Routes Pattern
// Alternative approach for optional params
<Routes>
<Route path="/users/:userId" element={<UserProfile />} />
<Route path="/users/:userId/posts" element={<UserPosts />} />
<Route path="/users/:userId/posts/:postId" element={<UserPost />} />
</Routes>
Working with Parameters
Fetching Data Based on Parameters
function UserProfile() {
const { userId } = useParams();
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
fetchUser(userId)
.then(data => setUser(data))
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, [userId]); // Re-fetch when userId changes
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
Parameter Validation
function ProductDetail() {
const { productId } = useParams();
const navigate = useNavigate();
// Validate parameter
useEffect(() => {
if (!productId || isNaN(productId)) {
navigate('/products', { replace: true });
return;
}
// Additional validation
if (parseInt(productId) < 1) {
navigate('/404');
}
}, [productId, navigate]);
return <div>Product ID: {productId}</div>;
}
Query Parameters
Using useSearchParams
import { useSearchParams } from 'react-router-dom';
function SearchResults() {
const [searchParams, setSearchParams] = useSearchParams();
// Get query parameters
const query = searchParams.get('q');
const page = searchParams.get('page') || 1;
const sort = searchParams.get('sort') || 'relevance';
// Update query parameters
const updatePage = (newPage) => {
setSearchParams(prev => {
prev.set('page', newPage);
return prev;
});
};
const updateSort = (newSort) => {
setSearchParams({
q: query,
sort: newSort,
page: 1 // Reset to first page
});
};
return (
<div>
<h1>Search Results for: {query}</h1>
<select value={sort} onChange={(e) => updateSort(e.target.value)}>
<option value="relevance">Relevance</option>
<option value="date">Date</option>
<option value="popularity">Popularity</option>
</select>
<p>Page {page}</p>
<button onClick={() => updatePage(parseInt(page) + 1)}>
Next Page
</button>
</div>
);
}
Building Query Strings
function FilteredProducts() {
const [filters, setFilters] = useState({
category: '',
minPrice: '',
maxPrice: '',
inStock: false
});
const navigate = useNavigate();
const applyFilters = () => {
const params = new URLSearchParams();
Object.entries(filters).forEach(([key, value]) => {
if (value) {
params.append(key, value);
}
});
navigate(`/products?${params.toString()}`);
};
return (
<div>
<input
placeholder="Category"
value={filters.category}
onChange={(e) => setFilters({...filters, category: e.target.value})}
/>
<input
type="number"
placeholder="Min Price"
value={filters.minPrice}
onChange={(e) => setFilters({...filters, minPrice: e.target.value})}
/>
<input
type="number"
placeholder="Max Price"
value={filters.maxPrice}
onChange={(e) => setFilters({...filters, maxPrice: e.target.value})}
/>
<label>
<input
type="checkbox"
checked={filters.inStock}
onChange={(e) => setFilters({...filters, inStock: e.target.checked})}
/>
In Stock Only
</label>
<button onClick={applyFilters}>Apply Filters</button>
</div>
);
}
Advanced Parameter Patterns
Nested Parameters
// Routes configuration
<Routes>
<Route path="/teams/:teamId" element={<Team />}>
<Route path="members/:memberId" element={<TeamMember />} />
</Route>
</Routes>
// Parent component
function Team() {
const { teamId } = useParams();
return (
<div>
<h1>Team {teamId}</h1>
<Outlet />
</div>
);
}
// Child component has access to all params
function TeamMember() {
const { teamId, memberId } = useParams();
return (
<div>
<p>Team: {teamId}</p>
<p>Member: {memberId}</p>
</div>
);
}
Wildcard Parameters
// Catch-all route
<Route path="/docs/*" element={<Documentation />} />
function Documentation() {
const params = useParams();
const path = params['*']; // Get everything after /docs/
return (
<div>
<h1>Documentation</h1>
<p>Path: {path}</p>
</div>
);
}
Parameter Constraints
// Custom route matching
function App() {
return (
<Routes>
{/* Only matches numeric IDs */}
<Route
path="/users/:id"
element={<UserProfile />}
// Custom matcher
loader={({ params }) => {
if (!/^\d+$/.test(params.id)) {
throw new Response("Not Found", { status: 404 });
}
return null;
}}
/>
</Routes>
);
}
Parameter Hooks and Utilities
Custom Parameter Hook
function useTypedParams() {
const params = useParams();
return {
...params,
// Type conversions
getNumber: (key) => {
const value = params[key];
return value ? parseInt(value, 10) : null;
},
getBoolean: (key) => {
const value = params[key];
return value === 'true';
},
getArray: (key, separator = ',') => {
const value = params[key];
return value ? value.split(separator) : [];
}
};
}
// Usage
function Component() {
const params = useTypedParams();
const userId = params.getNumber('userId');
const tags = params.getArray('tags');
return <div>User ID: {userId}</div>;
}
Parameter Validation Hook
function useValidatedParams(schema) {
const params = useParams();
const navigate = useNavigate();
const [validatedParams, setValidatedParams] = useState(null);
useEffect(() => {
try {
const validated = schema.validate(params);
setValidatedParams(validated);
} catch (error) {
console.error('Invalid parameters:', error);
navigate('/error');
}
}, [params, schema, navigate]);
return validatedParams;
}
// Usage with a simple schema
const userSchema = {
validate: (params) => {
if (!params.userId || isNaN(params.userId)) {
throw new Error('Invalid user ID');
}
return {
userId: parseInt(params.userId)
};
}
};
function UserProfile() {
const params = useValidatedParams(userSchema);
if (!params) return <div>Validating...</div>;
return <div>User ID: {params.userId}</div>;
}
Real-World Examples
E-commerce Product Page
function ProductPage() {
const { category, productSlug } = useParams();
const [searchParams] = useSearchParams();
const [product, setProduct] = useState(null);
// Get variant from query params
const selectedColor = searchParams.get('color');
const selectedSize = searchParams.get('size');
useEffect(() => {
fetchProduct(category, productSlug)
.then(setProduct)
.catch(console.error);
}, [category, productSlug]);
return (
<div>
{product && (
<>
<nav>
<Link to="/">Home</Link> /
<Link to={`/products/${category}`}>{category}</Link> /
<span>{product.name}</span>
</nav>
<h1>{product.name}</h1>
<div>
<h3>Colors:</h3>
{product.colors.map(color => (
<Link
key={color}
to={`?color=${color}&size=${selectedSize || ''}`}
className={selectedColor === color ? 'selected' : ''}
>
{color}
</Link>
))}
</div>
<div>
<h3>Sizes:</h3>
{product.sizes.map(size => (
<Link
key={size}
to={`?color=${selectedColor || ''}&size=${size}`}
className={selectedSize === size ? 'selected' : ''}
>
{size}
</Link>
))}
</div>
</>
)}
</div>
);
}
Blog with Categories and Tags
function BlogRoutes() {
return (
<Routes>
<Route path="/blog" element={<BlogList />} />
<Route path="/blog/category/:category" element={<BlogCategory />} />
<Route path="/blog/tag/:tag" element={<BlogTag />} />
<Route path="/blog/:year/:month/:slug" element={<BlogPost />} />
</Routes>
);
}
function BlogList() {
const [searchParams] = useSearchParams();
const page = searchParams.get('page') || 1;
const search = searchParams.get('search') || '';
const [posts, setPosts] = useState([]);
useEffect(() => {
fetchPosts({ page, search }).then(setPosts);
}, [page, search]);
return (
<div>
<h1>Blog</h1>
<input
placeholder="Search posts..."
defaultValue={search}
onKeyPress={(e) => {
if (e.key === 'Enter') {
const newSearch = e.target.value;
window.location.href = `/blog?search=${newSearch}`;
}
}}
/>
{posts.map(post => (
<article key={post.id}>
<h2>
<Link to={`/blog/${post.year}/${post.month}/${post.slug}`}>
{post.title}
</Link>
</h2>
<p>
Category:
<Link to={`/blog/category/${post.category}`}>
{post.category}
</Link>
</p>
<p>
Tags:
{post.tags.map(tag => (
<Link key={tag} to={`/blog/tag/${tag}`}>
#{tag}
</Link>
))}
</p>
</article>
))}
</div>
);
}
Best Practices
- Validate parameters - Don't trust URL params
- Handle missing params - Provide defaults or redirects
- Use semantic URLs - Make URLs human-readable
- Encode special characters - Use encodeURIComponent
- Keep URLs RESTful - Follow REST conventions
- Handle loading states - While fetching param-based data
- Update params carefully - Preserve other params when updating
- Document param formats - Especially for complex patterns
Route parameters are powerful for creating dynamic, data-driven applications. Use them wisely to build flexible and maintainable routing systems!