Welcome to Route Parameters!
Imagine a library where each book has a unique code. Instead of creating separate paths for every book, you use a pattern like /books/:bookCode. Route parameters are placeholders in your URL that capture dynamic values, making your routes flexible and reusable. Today, we'll master the art of dynamic routing!
graph LR
A[URL Path] --> B[Static Segments]
A --> C[Dynamic Parameters]
B --> D[/users/]
C --> E[:userId]
D --> F[/users/123]
E --> F
F --> G[{userId: '123'}]
style A fill:#ff9999
style C fill:#99ff99
style G fill:#9999ff
Route Parameters Fundamentals
1. Basic Parameter Usage
import { useParams } from 'react-router-dom';
// Route definition
<Route path="/users/:userId" element={<UserProfile />} />
<Route path="/posts/:postId/comments/:commentId" element={<Comment />} />
// Component using parameters
function UserProfile() {
const { userId } = useParams();
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetchUser(userId)
.then(userData => {
setUser(userData);
setError(null);
})
.catch(err => {
setError(err.message);
setUser(null);
})
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} />;
if (!user) return <NotFound />;
return (
<div className="user-profile">
<h1>{user.name}</h1>
<p>User ID: {userId}</p>
<UserDetails user={user} />
</div>
);
}
// Multiple parameters
function Comment() {
const { postId, commentId } = useParams();
return (
<div>
<h2>Comment #{commentId}</h2>
<p>On post: {postId}</p>
</div>
);
}
2. Optional Parameters
// Optional parameters with question mark
<Route path="/products/:category/:subcategory?" element={<Products />} />
function Products() {
const { category, subcategory } = useParams();
return (
<div>
<h1>{category} Products</h1>
{subcategory && <h2>Subcategory: {subcategory}</h2>}
<ProductList category={category} subcategory={subcategory} />
</div>
);
}
// Alternative approach using nested routes
<Route path="/products/:category" element={<CategoryLayout />}>
<Route index element={<CategoryProducts />} />
<Route path=":subcategory" element={<SubcategoryProducts />} />
</Route>
3. Wildcard Parameters
// Catch-all routes with asterisk
<Route path="/files/*" element={<FileExplorer />} />
<Route path="/docs/:section/*" element={<Documentation />} />
function FileExplorer() {
const params = useParams();
// params['*'] contains everything after /files/
const filePath = params['*'] || '';
return (
<div>
<h1>File Explorer</h1>
<p>Current path: /files/{filePath}</p>
<FileTree path={filePath} />
</div>
);
}
// More specific wildcard usage
function Documentation() {
const { section } = useParams();
const subPath = useParams()['*'];
return (
<div>
<h1>{section} Documentation</h1>
<p>Viewing: {subPath}</p>
<DocContent section={section} path={subPath} />
</div>
);
}
Advanced Parameter Patterns
1. Parameter Validation
// Custom hook for parameter validation
function useValidatedParams(schema) {
const params = useParams();
const navigate = useNavigate();
const [validatedParams, setValidatedParams] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
try {
const validated = schema.parse(params);
setValidatedParams(validated);
setError(null);
} catch (err) {
setError(err);
navigate('/error/invalid-params', {
state: { error: err.message }
});
}
}, [params, schema, navigate]);
return { params: validatedParams, error };
}
// Using with Zod schema
import { z } from 'zod';
const userSchema = z.object({
userId: z.string().regex(/^\d+$/).transform(Number),
});
function UserProfile() {
const { params, error } = useValidatedParams(userSchema);
if (error) return <InvalidParams error={error} />;
if (!params) return <Loading />;
return <UserContent userId={params.userId} />;
}
// Manual validation
function ProductPage() {
const { productId } = useParams();
const navigate = useNavigate();
useEffect(() => {
// Validate product ID format
if (!/^[A-Z0-9]{8}$/.test(productId)) {
navigate('/products/invalid-id', { replace: true });
}
}, [productId, navigate]);
return <ProductDetails productId={productId} />;
}
2. Dynamic Parameter Types
// Type conversion and parsing
function useTypedParams() {
const params = useParams();
return {
userId: parseInt(params.userId, 10),
price: parseFloat(params.price),
isActive: params.status === 'active',
tags: params.tags?.split(',') || [],
date: params.date ? new Date(params.date) : null,
};
}
// Complex parameter parsing
function useProductFilters() {
const params = useParams();
const searchParams = useSearchParams()[0];
const filters = {
category: params.category,
priceRange: {
min: parseFloat(searchParams.get('minPrice') || '0'),
max: parseFloat(searchParams.get('maxPrice') || 'Infinity'),
},
attributes: JSON.parse(searchParams.get('attrs') || '{}'),
sortBy: searchParams.get('sort') || 'relevance',
};
return filters;
}
// Using typed params
function ProductList() {
const filters = useProductFilters();
const [products, setProducts] = useState([]);
useEffect(() => {
fetchProducts(filters).then(setProducts);
}, [filters]);
return (
<div>
<h1>{filters.category} Products</h1>
<ProductGrid products={products} />
</div>
);
}
3. Parameter Patterns
// Complex route patterns
const routes = [
// Date-based routing
{
path: '/archive/:year/:month/:day?',
element: <ArchivePage />,
},
// Version-based API routes
{
path: '/api/v:version/:endpoint',
element: <APIEndpoint />,
},
// Multi-language support
{
path: '/:lang(en|es|fr)?/products/:id',
element: <ProductPage />,
},
// UUID patterns
{
path: '/items/:uuid([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})',
element: <ItemDetails />,
},
];
// Date-based component
function ArchivePage() {
const { year, month, day } = useParams();
const date = {
year: parseInt(year, 10),
month: parseInt(month, 10),
day: day ? parseInt(day, 10) : null,
};
const isValidDate = () => {
if (date.year < 1970 || date.year > new Date().getFullYear()) return false;
if (date.month < 1 || date.month > 12) return false;
if (date.day && (date.day < 1 || date.day > 31)) return false;
return true;
};
if (!isValidDate()) {
return <Navigate to="/archive" replace />;
}
return (
<div>
<h1>Archive for {year}/{month}{day ? `/${day}` : ''}</h1>
<ArchiveContent date={date} />
</div>
);
}
Dynamic Route Generation
1. Data-Driven Routes
// Generate routes from data
function DynamicRoutes() {
const [categories, setCategories] = useState([]);
useEffect(() => {
fetchCategories().then(setCategories);
}, []);
return (
<Routes>
<Route path="/shop" element={<ShopLayout />}>
<Route index element={<ShopHome />} />
{categories.map(category => (
<Route
key={category.slug}
path={category.slug}
element={<CategoryPage category={category} />}
>
<Route index element={<CategoryOverview />} />
{category.subcategories?.map(sub => (
<Route
key={sub.slug}
path={sub.slug}
element={<SubcategoryPage subcategory={sub} />}
/>
))}
</Route>
))}
</Route>
</Routes>
);
}
// Route configuration from API
function APIDefinedRoutes() {
const [routeConfig, setRouteConfig] = useState([]);
useEffect(() => {
fetchRouteConfig().then(setRouteConfig);
}, []);
const createRouteElement = (config) => {
switch (config.component) {
case 'ProductList':
return <ProductList {...config.props} />;
case 'CategoryPage':
return <CategoryPage {...config.props} />;
case 'ContentPage':
return <ContentPage {...config.props} />;
default:
return <GenericPage {...config.props} />;
}
};
return (
<Routes>
{routeConfig.map(route => (
<Route
key={route.path}
path={route.path}
element={createRouteElement(route)}
>
{route.children?.map(child => (
<Route
key={child.path}
path={child.path}
element={createRouteElement(child)}
/>
))}
</Route>
))}
</Routes>
);
}
2. Parameterized Components
// Reusable parameterized component
function ResourcePage({ resourceType }) {
const { id } = useParams();
const [resource, setResource] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetchResource(resourceType, id)
.then(setResource)
.finally(() => setLoading(false));
}, [resourceType, id]);
if (loading) return <Loading />;
if (!resource) return <NotFound />;
return (
<div className={`${resourceType}-page`}>
<h1>{resource.title}</h1>
<ResourceContent type={resourceType} data={resource} />
</div>
);
}
// Usage with different resource types
<Routes>
<Route
path="/articles/:id"
element={<ResourcePage resourceType="article" />}
/>
<Route
path="/videos/:id"
element={<ResourcePage resourceType="video" />}
/>
<Route
path="/podcasts/:id"
element={<ResourcePage resourceType="podcast" />}
/>
</Routes>
// Generic CRUD routes
function CRUDRoutes({ resourceName, components }) {
return (
<Routes>
<Route path={`/${resourceName}`} element={components.List} />
<Route path={`/${resourceName}/new`} element={components.Create} />
<Route path={`/${resourceName}/:id`} element={components.View} />
<Route path={`/${resourceName}/:id/edit`} element={components.Edit} />
</Routes>
);
}
// Usage
<CRUDRoutes
resourceName="products"
components={{
List: ProductList,
Create: CreateProduct,
View: ProductDetails,
Edit: EditProduct
}}
/>
Combining Route and Query Parameters
1. Search and Filter Implementation
// Advanced search with route and query params
function SearchResults() {
const { category } = useParams();
const [searchParams, setSearchParams] = useSearchParams();
const filters = {
query: searchParams.get('q') || '',
sort: searchParams.get('sort') || 'relevance',
page: parseInt(searchParams.get('page') || '1', 10),
priceMin: parseFloat(searchParams.get('priceMin') || '0'),
priceMax: parseFloat(searchParams.get('priceMax') || 'Infinity'),
inStock: searchParams.get('inStock') === 'true',
brands: searchParams.getAll('brand'),
attributes: Object.fromEntries(
Array.from(searchParams.entries())
.filter(([key]) => key.startsWith('attr_'))
.map(([key, value]) => [key.replace('attr_', ''), value])
),
};
const updateFilter = (key, value) => {
setSearchParams(prev => {
const newParams = new URLSearchParams(prev);
if (value === null || value === undefined) {
newParams.delete(key);
} else if (Array.isArray(value)) {
newParams.delete(key);
value.forEach(v => newParams.append(key, v));
} else {
newParams.set(key, String(value));
}
return newParams;
});
};
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
searchProducts(category, filters)
.then(setResults)
.finally(() => setLoading(false));
}, [category, filters]);
return (
<div className="search-results">
<SearchFilters
filters={filters}
onFilterChange={updateFilter}
category={category}
/>
<div className="results">
<h1>
{filters.query
? `Search results for "${filters.query}" in ${category}`
: `All ${category} products`}
</h1>
{loading ? (
<LoadingSpinner />
) : (
<ProductGrid products={results} />
)}
</div>
<Pagination
currentPage={filters.page}
totalPages={results.totalPages}
onPageChange={(page) => updateFilter('page', page)}
/>
</div>
);
}
2. State Synchronization
// Sync component state with URL
function useSyncedState(paramName, initialValue, parser = JSON.parse) {
const [searchParams, setSearchParams] = useSearchParams();
const setValue = (newValue) => {
setSearchParams(prev => {
const params = new URLSearchParams(prev);
if (newValue === null || newValue === undefined) {
params.delete(paramName);
} else {
params.set(paramName, JSON.stringify(newValue));
}
return params;
});
};
const value = searchParams.has(paramName)
? parser(searchParams.get(paramName))
: initialValue;
return [value, setValue];
}
// Using synced state
function FilterableTable() {
const { dataType } = useParams();
const [filters, setFilters] = useSyncedState('filters', {});
const [sortConfig, setSortConfig] = useSyncedState('sort', {
field: 'id',
direction: 'asc'
});
const [page, setPage] = useSyncedState('page', 1, parseInt);
const [data, setData] = useState([]);
useEffect(() => {
fetchData(dataType, { filters, sort: sortConfig, page })
.then(setData);
}, [dataType, filters, sortConfig, page]);
return (
<div>
<FilterPanel
filters={filters}
onChange={setFilters}
/>
<DataTable
data={data}
sortConfig={sortConfig}
onSort={setSortConfig}
/>
<Pagination
currentPage={page}
onPageChange={setPage}
/>
</div>
);
}
Parameter Error Handling
1. Invalid Parameter Handling
// Comprehensive error handling
function ProductPage() {
const { productId } = useParams();
const navigate = useNavigate();
const [product, setProduct] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const validateAndFetch = async () => {
setLoading(true);
setError(null);
try {
// Validate format
if (!/^PROD-\d{6}$/.test(productId)) {
throw new Error('Invalid product ID format');
}
// Fetch product
const data = await fetchProduct(productId);
if (!data) {
throw new Error('Product not found');
}
setProduct(data);
} catch (err) {
setError(err);
// Handle different error types
if (err.message === 'Invalid product ID format') {
navigate('/products/invalid-format', {
state: { invalidId: productId }
});
} else if (err.message === 'Product not found') {
navigate('/products/not-found', {
state: { productId }
});
} else {
navigate('/error', {
state: { error: err.message }
});
}
} finally {
setLoading(false);
}
};
validateAndFetch();
}, [productId, navigate]);
if (loading) return <Loading />;
if (error) return <ErrorDisplay error={error} />;
if (!product) return null;
return <ProductDetails product={product} />;
}
// Error recovery component
function ParameterErrorBoundary({ children }) {
const location = useLocation();
const navigate = useNavigate();
const [hasError, setHasError] = useState(false);
useEffect(() => {
setHasError(false);
}, [location.pathname]);
const handleError = (error) => {
setHasError(true);
// Log error
console.error('Parameter error:', error);
// Attempt recovery
if (error.type === 'INVALID_PARAM') {
navigate('/search', {
state: {
error: 'Invalid URL parameter',
attemptedPath: location.pathname
}
});
}
};
if (hasError) {
return (
<div className="error-boundary">
<h2>Invalid URL Parameters</h2>
<p>The URL contains invalid parameters.</p>
<button onClick={() => navigate('/')}>
Go to Home
</button>
</div>
);
}
return (
<ErrorBoundary onError={handleError}>
{children}
</ErrorBoundary>
);
}
2. Fallback Strategies
// Implement fallback for missing parameters
function SmartProductPage() {
const { productId, categoryId } = useParams();
const navigate = useNavigate();
useEffect(() => {
const handleMissingParams = async () => {
if (!productId && categoryId) {
// Redirect to category page if only product ID missing
navigate(`/categories/${categoryId}`);
} else if (productId && !categoryId) {
// Try to find category for product
try {
const product = await fetchProduct(productId);
if (product?.categoryId) {
navigate(`/categories/${product.categoryId}/products/${productId}`);
}
} catch (err) {
navigate('/products/search', {
state: { query: productId }
});
}
} else if (!productId && !categoryId) {
// No parameters, go to home
navigate('/');
}
};
handleMissingParams();
}, [productId, categoryId, navigate]);
if (!productId || !categoryId) return null;
return <ProductDisplay productId={productId} categoryId={categoryId} />;
}
Testing Routes with Parameters
// Testing parameterized routes
import { render, screen } from '@testing-library/react';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
describe('Parameterized Routes', () => {
test('extracts route parameters correctly', () => {
const TestComponent = () => {
const { userId, postId } = useParams();
return (
<div>
<span data-testid="userId">{userId}</span>
<span data-testid="postId">{postId}</span>
</div>
);
};
render(
<MemoryRouter initialEntries={['/users/123/posts/456']}>
<Routes>
<Route
path="/users/:userId/posts/:postId"
element={<TestComponent />}
/>
</Routes>
</MemoryRouter>
);
expect(screen.getByTestId('userId')).toHaveTextContent('123');
expect(screen.getByTestId('postId')).toHaveTextContent('456');
});
test('handles optional parameters', () => {
const TestComponent = () => {
const { category, subcategory } = useParams();
return (
<div>
<span data-testid="category">{category}</span>
<span data-testid="subcategory">{subcategory || 'none'}</span>
</div>
);
};
const { rerender } = render(
<MemoryRouter initialEntries={['/products/electronics']}>
<Routes>
<Route
path="/products/:category/:subcategory?"
element={<TestComponent />}
/>
</Routes>
</MemoryRouter>
);
expect(screen.getByTestId('category')).toHaveTextContent('electronics');
expect(screen.getByTestId('subcategory')).toHaveTextContent('none');
// Test with subcategory
rerender(
<MemoryRouter initialEntries={['/products/electronics/phones']}>
<Routes>
<Route
path="/products/:category/:subcategory?"
element={<TestComponent />}
/>
</Routes>
</MemoryRouter>
);
expect(screen.getByTestId('subcategory')).toHaveTextContent('phones');
});
test('validates parameters', async () => {
const TestComponent = () => {
const { id } = useParams();
const navigate = useNavigate();
useEffect(() => {
if (!/^\d+$/.test(id)) {
navigate('/error');
}
}, [id, navigate]);
return <div>Valid ID: {id}</div>;
};
const ErrorComponent = () => <div>Error Page</div>;
render(
<MemoryRouter initialEntries={['/items/abc']}>
<Routes>
<Route path="/items/:id" element={<TestComponent />} />
<Route path="/error" element={<ErrorComponent />} />
</Routes>
</MemoryRouter>
);
// Should redirect to error page for invalid ID
expect(await screen.findByText('Error Page')).toBeInTheDocument();
});
});
Practice Exercises
Exercise 1: Build a Blog with Dynamic Routes
Create a blog system with:
- Post listing with pagination (/blog/page/:pageNumber)
- Individual posts (/blog/:year/:month/:slug)
- Category filtering (/blog/category/:categorySlug)
- Author pages (/blog/author/:authorId)
- Tag filtering with multiple tags (/blog/tags/:tag1/:tag2?/:tag3?)
Exercise 2: E-commerce Product Browser
Implement a product browsing system:
- Category hierarchy (/shop/:category/:subcategory?/:subsubcategory?)
- Product details (/shop/product/:productId)
- Search with filters (/shop/search?q=:query&brand=:brand&price=:range)
- Compare products (/shop/compare/:id1/:id2/:id3?)
Exercise 3: Multi-tenant Application
Create a multi-tenant app with:
- Tenant-specific routes (/:tenantId/dashboard)
- Resource management (/:tenantId/:resource/:resourceId)
- User profiles (/:tenantId/users/:userId)
- Settings (/:tenantId/settings/:section?)
Route Parameters Best Practices
1. Use Meaningful Parameter Names
// Good: Descriptive parameter names
<Route path="/users/:userId/posts/:postId" />
<Route path="/products/:productSlug" />
// Bad: Generic or unclear names
<Route path="/u/:id/p/:id2" />
<Route path="/item/:thing" />
2. Validate Parameters Early
function Component() {
const { id } = useParams();
// Validate immediately
if (!isValidId(id)) {
return <Redirect to="/error" />;
}
// Continue with valid ID...
}
3. Handle Loading States
function DataComponent() {
const { id } = useParams();
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetchData(id)
.then(setData)
.finally(() => setLoading(false));
}, [id]);
if (loading) return <LoadingSpinner />;
return <DataDisplay data={data} />;
}
Key Takeaways
- Route parameters make URLs dynamic and flexible
- Always validate parameters before using them
- Combine route and query parameters for complex filtering
- Handle missing or invalid parameters gracefully
- Consider SEO implications of parameter structure
- Test parameter handling thoroughly