Route Parameters Usage

Creating Dynamic and Flexible URLs

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