Skip to content

动态路由与参数获取

概述

动态路由是现代Web应用的核心特性,允许根据URL参数动态渲染不同的内容。React Router v6提供了强大的参数获取和处理机制,包括路径参数、查询参数、状态传递等多种方式。

路径参数详解

基础路径参数

jsx
import { useParams, Link } from 'react-router-dom';

// 路由定义
<Routes>
  <Route path="/users/:userId" element={<UserProfile />} />
  <Route path="/products/:category/:productId" element={<ProductDetail />} />
  <Route path="/blog/:year/:month/:day/:slug" element={<BlogPost />} />
</Routes>

// 获取单个参数
function UserProfile() {
  const { userId } = useParams();
  
  // userId 总是字符串类型,需要转换
  const numericUserId = parseInt(userId, 10);
  
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUser(numericUserId)
      .then(setUser)
      .catch(console.error)
      .finally(() => setLoading(false));
  }, [numericUserId]);

  if (loading) return <div>Loading user...</div>;
  if (!user) return <div>User not found</div>;

  return (
    <div className="user-profile">
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
      <p>User ID: {userId}</p>
      
      {/* 相关链接 */}
      <nav>
        <Link to={`/users/${userId}/posts`}>Posts</Link>
        <Link to={`/users/${userId}/followers`}>Followers</Link>
        <Link to={`/users/${userId}/settings`}>Settings</Link>
      </nav>
    </div>
  );
}

// 获取多个参数
function ProductDetail() {
  const { category, productId } = useParams();
  const [product, setProduct] = useState(null);

  useEffect(() => {
    fetchProductByCategory(category, productId)
      .then(setProduct);
  }, [category, productId]);

  return (
    <div className="product-detail">
      <nav className="breadcrumb">
        <Link to="/products">Products</Link>
        <span> / </span>
        <Link to={`/products/category/${category}`}>{category}</Link>
        <span> / </span>
        <span>{product?.name}</span>
      </nav>

      {product && (
        <div className="product-info">
          <h1>{product.name}</h1>
          <p>Category: {category}</p>
          <p>Product ID: {productId}</p>
          <p>Price: ${product.price}</p>
        </div>
      )}
    </div>
  );
}

// 复杂路径参数
function BlogPost() {
  const { year, month, day, slug } = useParams();
  
  const postDate = new Date(`${year}-${month}-${day}`);
  const formattedDate = postDate.toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });

  const [post, setPost] = useState(null);

  useEffect(() => {
    fetchPostByDate(year, month, day, slug)
      .then(setPost);
  }, [year, month, day, slug]);

  return (
    <article className="blog-post">
      <header>
        <h1>{post?.title}</h1>
        <time dateTime={`${year}-${month}-${day}`}>
          Published on {formattedDate}
        </time>
        <div className="post-meta">
          <Link to={`/blog/${year}`}>Posts from {year}</Link>
          <Link to={`/blog/${year}/${month}`}>Posts from {formattedDate.split(' ')[0]} {year}</Link>
        </div>
      </header>
      
      {post && (
        <div className="post-content">
          <div dangerouslySetInnerHTML={{ __html: post.content }} />
        </div>
      )}
    </article>
  );
}

可选参数

jsx
// 路由定义 - 使用 ? 表示可选参数
<Routes>
  <Route path="/blog/:year?/:month?/:day?" element={<BlogArchive />} />
  <Route path="/shop/:category?/:subcategory?" element={<Shop />} />
</Routes>

function BlogArchive() {
  const { year, month, day } = useParams();
  const [posts, setPosts] = useState([]);

  // 根据参数构建过滤条件
  const buildDateFilter = useCallback(() => {
    const filter = {};
    
    if (year) {
      filter.year = parseInt(year, 10);
      
      if (month) {
        filter.month = parseInt(month, 10);
        
        if (day) {
          filter.day = parseInt(day, 10);
        }
      }
    }
    
    return filter;
  }, [year, month, day]);

  useEffect(() => {
    const dateFilter = buildDateFilter();
    fetchPosts(dateFilter).then(setPosts);
  }, [buildDateFilter]);

  const getArchiveTitle = () => {
    if (day && month && year) {
      return `Posts from ${month}/${day}/${year}`;
    } else if (month && year) {
      return `Posts from ${month}/${year}`;
    } else if (year) {
      return `Posts from ${year}`;
    } else {
      return 'All Posts';
    }
  };

  return (
    <div className="blog-archive">
      <header>
        <h1>{getArchiveTitle()}</h1>
        
        {/* 导航链接 */}
        <nav className="archive-nav">
          <Link to="/blog">All Years</Link>
          {year && (
            <>
              <span> / </span>
              <Link to={`/blog/${year}`}>{year}</Link>
            </>
          )}
          {month && year && (
            <>
              <span> / </span>
              <Link to={`/blog/${year}/${month}`}>{month}</Link>
            </>
          )}
          {day && month && year && (
            <>
              <span> / </span>
              <span>{day}</span>
            </>
          )}
        </nav>
      </header>

      <div className="posts-list">
        {posts.map(post => (
          <article key={post.id} className="post-preview">
            <h2>
              <Link to={`/blog/${post.year}/${post.month}/${post.day}/${post.slug}`}>
                {post.title}
              </Link>
            </h2>
            <time>{post.publishedAt}</time>
            <p>{post.excerpt}</p>
          </article>
        ))}
      </div>
    </div>
  );
}

// 商店页面with可选参数
function Shop() {
  const { category, subcategory } = useParams();
  const [products, setProducts] = useState([]);
  const [categories, setCategories] = useState([]);

  useEffect(() => {
    // 根据参数获取产品
    const filters = {};
    if (category) filters.category = category;
    if (subcategory) filters.subcategory = subcategory;
    
    fetchProducts(filters).then(setProducts);
  }, [category, subcategory]);

  useEffect(() => {
    // 获取分类列表
    fetchCategories().then(setCategories);
  }, []);

  return (
    <div className="shop">
      <aside className="categories-sidebar">
        <h3>Categories</h3>
        <ul>
          <li>
            <Link to="/shop" className={!category ? 'active' : ''}>
              All Products
            </Link>
          </li>
          {categories.map(cat => (
            <li key={cat.id}>
              <Link 
                to={`/shop/${cat.slug}`}
                className={category === cat.slug ? 'active' : ''}
              >
                {cat.name}
              </Link>
              
              {/* 子分类 */}
              {category === cat.slug && cat.subcategories && (
                <ul className="subcategories">
                  {cat.subcategories.map(subcat => (
                    <li key={subcat.id}>
                      <Link 
                        to={`/shop/${cat.slug}/${subcat.slug}`}
                        className={subcategory === subcat.slug ? 'active' : ''}
                      >
                        {subcat.name}
                      </Link>
                    </li>
                  ))}
                </ul>
              )}
            </li>
          ))}
        </ul>
      </aside>

      <main className="products-main">
        <header>
          <h1>
            {subcategory ? `${subcategory} in ${category}` :
             category ? category :
             'All Products'}
          </h1>
          <p>{products.length} products found</p>
        </header>

        <ProductGrid products={products} />
      </main>
    </div>
  );
}

参数验证和转换

jsx
// 自定义Hook进行参数验证
function useValidatedParams(schema) {
  const params = useParams();
  const navigate = useNavigate();

  const validatedParams = useMemo(() => {
    const result = {};
    const errors = [];

    Object.entries(schema).forEach(([key, validator]) => {
      const value = params[key];
      
      try {
        result[key] = validator(value);
      } catch (error) {
        errors.push({ key, error: error.message });
      }
    });

    if (errors.length > 0) {
      console.error('Parameter validation errors:', errors);
      navigate('/404', { replace: true });
      return null;
    }

    return result;
  }, [params, schema, navigate]);

  return validatedParams;
}

// 验证器函数
const validators = {
  positiveInteger: (value) => {
    const num = parseInt(value, 10);
    if (isNaN(num) || num <= 0) {
      throw new Error('Must be a positive integer');
    }
    return num;
  },
  
  slug: (value) => {
    if (!/^[a-z0-9-]+$/.test(value)) {
      throw new Error('Invalid slug format');
    }
    return value;
  },
  
  date: (value) => {
    const date = new Date(value);
    if (isNaN(date.getTime())) {
      throw new Error('Invalid date format');
    }
    return date;
  }
};

// 使用参数验证
function ValidatedUserProfile() {
  const params = useValidatedParams({
    userId: validators.positiveInteger
  });

  if (!params) return null; // 验证失败,会重定向

  const { userId } = params;

  return (
    <div>
      <h1>User #{userId}</h1>
    </div>
  );
}

function ValidatedBlogPost() {
  const params = useValidatedParams({
    year: (value) => {
      const year = parseInt(value, 10);
      const currentYear = new Date().getFullYear();
      if (year < 2000 || year > currentYear) {
        throw new Error('Year must be between 2000 and current year');
      }
      return year;
    },
    month: (value) => {
      const month = parseInt(value, 10);
      if (month < 1 || month > 12) {
        throw new Error('Month must be between 1 and 12');
      }
      return month;
    },
    slug: validators.slug
  });

  // 使用验证后的参数...
}

查询参数管理

useSearchParams详解

jsx
import { useSearchParams, useNavigate } from 'react-router-dom';

function ProductsPage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const navigate = useNavigate();

  // 获取查询参数
  const currentFilters = {
    category: searchParams.get('category') || '',
    minPrice: parseFloat(searchParams.get('minPrice')) || 0,
    maxPrice: parseFloat(searchParams.get('maxPrice')) || 1000,
    sortBy: searchParams.get('sortBy') || 'name',
    order: searchParams.get('order') || 'asc',
    page: parseInt(searchParams.get('page')) || 1,
    limit: parseInt(searchParams.get('limit')) || 12,
    inStock: searchParams.get('inStock') === 'true',
    onSale: searchParams.get('onSale') === 'true',
    brand: searchParams.getAll('brand') // 获取多个同名参数
  };

  // 更新单个参数
  const updateFilter = (key, value) => {
    const newParams = new URLSearchParams(searchParams);
    
    if (value === '' || value === null || value === undefined) {
      newParams.delete(key);
    } else {
      newParams.set(key, value.toString());
    }
    
    // 重置页码
    if (key !== 'page') {
      newParams.set('page', '1');
    }
    
    setSearchParams(newParams);
  };

  // 批量更新参数
  const updateFilters = (updates) => {
    setSearchParams(prev => {
      const newParams = new URLSearchParams(prev);
      
      Object.entries(updates).forEach(([key, value]) => {
        if (value === '' || 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, value.toString());
        }
      });
      
      return newParams;
    });
  };

  // 清除特定过滤器
  const clearFilter = (key) => {
    setSearchParams(prev => {
      const newParams = new URLSearchParams(prev);
      newParams.delete(key);
      return newParams;
    });
  };

  // 清除所有过滤器
  const clearAllFilters = () => {
    setSearchParams({});
  };

  // 添加到收藏夹(保持当前过滤器)
  const addToFavorites = () => {
    const currentUrl = `${location.pathname}?${searchParams.toString()}`;
    addFavoriteUrl(currentUrl, 'Filtered Products');
  };

  return (
    <div className="products-page">
      {/* 过滤器区域 */}
      <aside className="filters-sidebar">
        <h3>Filters</h3>
        
        {/* 分类过滤器 */}
        <div className="filter-group">
          <label>Category</label>
          <select
            value={currentFilters.category}
            onChange={(e) => updateFilter('category', e.target.value)}
          >
            <option value="">All Categories</option>
            <option value="electronics">Electronics</option>
            <option value="clothing">Clothing</option>
            <option value="books">Books</option>
          </select>
        </div>

        {/* 价格范围 */}
        <div className="filter-group">
          <label>Price Range</label>
          <input
            type="number"
            placeholder="Min Price"
            value={currentFilters.minPrice}
            onChange={(e) => updateFilter('minPrice', e.target.value)}
          />
          <input
            type="number"
            placeholder="Max Price"
            value={currentFilters.maxPrice}
            onChange={(e) => updateFilter('maxPrice', e.target.value)}
          />
        </div>

        {/* 复选框过滤器 */}
        <div className="filter-group">
          <label>
            <input
              type="checkbox"
              checked={currentFilters.inStock}
              onChange={(e) => updateFilter('inStock', e.target.checked)}
            />
            In Stock Only
          </label>
          
          <label>
            <input
              type="checkbox"
              checked={currentFilters.onSale}
              onChange={(e) => updateFilter('onSale', e.target.checked)}
            />
            On Sale
          </label>
        </div>

        {/* 多选过滤器 */}
        <div className="filter-group">
          <label>Brands</label>
          <div className="checkbox-group">
            {['Apple', 'Samsung', 'Google', 'Microsoft'].map(brand => (
              <label key={brand}>
                <input
                  type="checkbox"
                  checked={currentFilters.brand.includes(brand)}
                  onChange={(e) => {
                    const newBrands = e.target.checked
                      ? [...currentFilters.brand, brand]
                      : currentFilters.brand.filter(b => b !== brand);
                    updateFilter('brand', newBrands);
                  }}
                />
                {brand}
              </label>
            ))}
          </div>
        </div>

        <div className="filter-actions">
          <button onClick={clearAllFilters}>Clear All</button>
          <button onClick={addToFavorites}>Save Filters</button>
        </div>
      </aside>

      {/* 产品列表区域 */}
      <main className="products-main">
        <header className="products-header">
          <h1>Products</h1>
          
          {/* 排序控件 */}
          <div className="sort-controls">
            <select
              value={`${currentFilters.sortBy}-${currentFilters.order}`}
              onChange={(e) => {
                const [sortBy, order] = e.target.value.split('-');
                updateFilters({ sortBy, order });
              }}
            >
              <option value="name-asc">Name (A-Z)</option>
              <option value="name-desc">Name (Z-A)</option>
              <option value="price-asc">Price (Low to High)</option>
              <option value="price-desc">Price (High to Low)</option>
              <option value="date-desc">Newest First</option>
              <option value="rating-desc">Highest Rated</option>
            </select>
          </div>

          {/* 显示数量控件 */}
          <div className="limit-controls">
            <label>Show: </label>
            <select
              value={currentFilters.limit}
              onChange={(e) => updateFilter('limit', e.target.value)}
            >
              <option value="12">12 per page</option>
              <option value="24">24 per page</option>
              <option value="48">48 per page</option>
            </select>
          </div>
        </header>

        {/* 活跃过滤器显示 */}
        <ActiveFilters 
          filters={currentFilters} 
          onRemoveFilter={clearFilter}
        />

        {/* 产品网格 */}
        <ProductGrid 
          products={products}
          loading={loading}
        />

        {/* 分页 */}
        <Pagination
          currentPage={currentFilters.page}
          totalPages={totalPages}
          onPageChange={(page) => updateFilter('page', page)}
        />
      </main>
    </div>
  );
}

// 活跃过滤器组件
function ActiveFilters({ filters, onRemoveFilter }) {
  const activeFilters = Object.entries(filters)
    .filter(([key, value]) => {
      if (key === 'page' || key === 'limit') return false;
      if (Array.isArray(value)) return value.length > 0;
      return value !== '' && value !== 0 && value !== false;
    });

  if (activeFilters.length === 0) return null;

  return (
    <div className="active-filters">
      <span>Active filters:</span>
      {activeFilters.map(([key, value]) => (
        <div key={key} className="filter-tag">
          <span>{key}: {Array.isArray(value) ? value.join(', ') : value.toString()}</span>
          <button onClick={() => onRemoveFilter(key)}>×</button>
        </div>
      ))}
    </div>
  );
}

查询参数持久化

jsx
// 自定义Hook:持久化查询参数
function usePersistentSearchParams(storageKey, defaultParams = {}) {
  const [searchParams, setSearchParams] = useSearchParams();
  
  // 从localStorage恢复参数
  useEffect(() => {
    const saved = localStorage.getItem(storageKey);
    if (saved) {
      try {
        const savedParams = JSON.parse(saved);
        const urlParams = new URLSearchParams();
        
        Object.entries({...defaultParams, ...savedParams}).forEach(([key, value]) => {
          if (value !== '' && value !== null && value !== undefined) {
            urlParams.set(key, value.toString());
          }
        });
        
        setSearchParams(urlParams, { replace: true });
      } catch (error) {
        console.error('Failed to restore search params:', error);
      }
    }
  }, [storageKey, setSearchParams]);

  // 保存参数到localStorage
  useEffect(() => {
    const paramsObject = Object.fromEntries(searchParams);
    localStorage.setItem(storageKey, JSON.stringify(paramsObject));
  }, [searchParams, storageKey]);

  return [searchParams, setSearchParams];
}

// 使用持久化参数
function ProductsWithPersistentFilters() {
  const [searchParams, setSearchParams] = usePersistentSearchParams(
    'products-filters',
    { sortBy: 'name', order: 'asc', limit: '12' }
  );

  // 其余逻辑...
}

动态路由生成

基于数据的路由

jsx
// 动态生成路由配置
function createDynamicRoutes(categories, userRole) {
  const routes = [
    {
      path: '/',
      element: <Layout />,
      children: [
        { index: true, element: <Home /> }
      ]
    }
  ];

  // 根据分类数据生成路由
  const categoryRoutes = categories.map(category => ({
    path: `category/${category.slug}`,
    element: <CategoryPage />,
    loader: ({ params }) => loadCategoryData(params.slug),
    children: category.subcategories?.map(sub => ({
      path: sub.slug,
      element: <SubcategoryPage />,
      loader: ({ params }) => loadSubcategoryData(params)
    })) || []
  }));

  routes[0].children.push(...categoryRoutes);

  // 根据用户角色添加路由
  if (userRole === 'admin') {
    routes[0].children.push({
      path: 'admin',
      element: <AdminPanel />,
      children: [
        { index: true, element: <AdminDashboard /> },
        { path: 'categories', element: <ManageCategories /> }
      ]
    });
  }

  return routes;
}

// 动态路由Provider
function DynamicRoutesProvider({ children }) {
  const [categories, setCategories] = useState([]);
  const [userRole, setUserRole] = useState(null);
  const [router, setRouter] = useState(null);

  useEffect(() => {
    Promise.all([
      fetchCategories(),
      fetchUserRole()
    ]).then(([cats, role]) => {
      setCategories(cats);
      setUserRole(role);
      
      const routes = createDynamicRoutes(cats, role);
      setRouter(createBrowserRouter(routes));
    });
  }, []);

  if (!router) return <div>Loading application...</div>;

  return <RouterProvider router={router} />;
}

条件路由渲染

jsx
// 基于功能标志的路由
function FeatureFlagRoutes() {
  const { features } = useFeatureFlags();

  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Home />} />
        
        {/* 基础功能路由 */}
        <Route path="products" element={<Products />} />
        <Route path="about" element={<About />} />
        
        {/* 条件路由 - 只在功能启用时显示 */}
        {features.analytics && (
          <Route path="analytics" element={<Analytics />} />
        )}
        
        {features.advanced_search && (
          <Route path="search" element={<AdvancedSearch />} />
        )}
        
        {features.social_features && (
          <Route path="social" element={<SocialLayout />}>
            <Route index element={<SocialFeed />} />
            <Route path="friends" element={<Friends />} />
            <Route path="messages" element={<Messages />} />
          </Route>
        )}
        
        {/* 基于用户类型的路由 */}
        <Route path="dashboard" element={<DashboardSelector />} />
      </Route>
    </Routes>
  );
}

function DashboardSelector() {
  const { user } = useAuth();

  // 根据用户类型渲染不同的仪表板
  switch (user.type) {
    case 'admin':
      return <AdminDashboard />;
    case 'manager':
      return <ManagerDashboard />;
    case 'customer':
      return <CustomerDashboard />;
    default:
      return <Navigate to="/login" replace />;
  }
}

// 实验性路由
function ExperimentalRoutes() {
  const { experiments } = useExperiments();

  return (
    <Routes>
      <Route path="/experiment" element={<ExperimentLayout />}>
        {experiments.includes('new-ui') && (
          <Route path="new-ui" element={<NewUIExperiment />} />
        )}
        
        {experiments.includes('beta-features') && (
          <Route path="beta" element={<BetaFeatures />} />
        )}
      </Route>
    </Routes>
  );
}

高级参数模式

参数解析和验证

jsx
// 复合参数解析
function useCompoundParams() {
  const { compoundParam } = useParams();
  
  const parseCompoundParam = useCallback((param) => {
    if (!param) return null;
    
    // 解析复合参数:userId-postId-commentId
    const parts = param.split('-');
    
    if (parts.length !== 3) {
      throw new Error('Invalid compound parameter format');
    }
    
    return {
      userId: parseInt(parts[0], 10),
      postId: parseInt(parts[1], 10),
      commentId: parseInt(parts[2], 10)
    };
  }, []);

  const params = useMemo(() => {
    try {
      return parseCompoundParam(compoundParam);
    } catch (error) {
      console.error('Parameter parsing error:', error);
      return null;
    }
  }, [compoundParam, parseCompoundParam]);

  return params;
}

// 路由定义
<Route path="/thread/:compoundParam" element={<ThreadDetail />} />

// 使用
function ThreadDetail() {
  const params = useCompoundParams();
  const navigate = useNavigate();

  if (!params) {
    navigate('/404', { replace: true });
    return null;
  }

  const { userId, postId, commentId } = params;

  return (
    <div>
      <h1>Comment Detail</h1>
      <p>User ID: {userId}</p>
      <p>Post ID: {postId}</p>
      <p>Comment ID: {commentId}</p>
    </div>
  );
}

参数变换和映射

jsx
// 参数变换Hook
function useTransformedParams(transformers) {
  const rawParams = useParams();
  
  return useMemo(() => {
    const transformed = {};
    
    Object.entries(rawParams).forEach(([key, value]) => {
      const transformer = transformers[key];
      transformed[key] = transformer ? transformer(value) : value;
    });
    
    return transformed;
  }, [rawParams, transformers]);
}

// 参数变换器
const paramTransformers = {
  userId: (value) => parseInt(value, 10),
  slug: (value) => decodeURIComponent(value),
  date: (value) => new Date(value),
  coordinates: (value) => {
    const [lat, lng] = value.split(',');
    return {
      latitude: parseFloat(lat),
      longitude: parseFloat(lng)
    };
  },
  tags: (value) => value.split('+').map(decodeURIComponent)
};

// 使用变换后的参数
function TransformedParamsComponent() {
  const params = useTransformedParams(paramTransformers);
  
  // params.userId 现在是数字类型
  // params.date 现在是Date对象
  // params.coordinates 现在是对象 {latitude, longitude}
  
  return (
    <div>
      <p>User ID: {params.userId}</p>
      <p>Date: {params.date?.toLocaleDateString()}</p>
      <p>Location: {params.coordinates?.latitude}, {params.coordinates?.longitude}</p>
    </div>
  );
}

参数同步到状态

jsx
// 将URL参数同步到组件状态
function useParamsSync(paramKey, defaultValue, parser = (v) => v) {
  const [searchParams, setSearchParams] = useSearchParams();
  const paramValue = searchParams.get(paramKey);

  const [localState, setLocalState] = useState(() => {
    return paramValue ? parser(paramValue) : defaultValue;
  });

  // 参数变化时更新本地状态
  useEffect(() => {
    if (paramValue !== null) {
      setLocalState(parser(paramValue));
    }
  }, [paramValue, parser]);

  // 更新函数同时更新URL和本地状态
  const updateValue = useCallback((newValue) => {
    setLocalState(newValue);
    
    if (newValue === defaultValue) {
      // 如果是默认值,从URL中移除参数
      setSearchParams(prev => {
        const newParams = new URLSearchParams(prev);
        newParams.delete(paramKey);
        return newParams;
      });
    } else {
      setSearchParams(prev => {
        const newParams = new URLSearchParams(prev);
        newParams.set(paramKey, newValue.toString());
        return newParams;
      });
    }
  }, [paramKey, defaultValue, setSearchParams]);

  return [localState, updateValue];
}

// 使用参数同步
function SearchPage() {
  const [query, setQuery] = useParamsSync('q', '');
  const [sortBy, setSortBy] = useParamsSync('sort', 'relevance');
  const [page, setPage] = useParamsSync('page', 1, parseInt);

  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  // 参数变化时执行搜索
  useEffect(() => {
    if (query) {
      setLoading(true);
      searchAPI(query, { sortBy, page })
        .then(setResults)
        .finally(() => setLoading(false));
    }
  }, [query, sortBy, page]);

  return (
    <div className="search-page">
      <header className="search-header">
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Search..."
        />
        
        <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
          <option value="relevance">Most Relevant</option>
          <option value="date">Newest</option>
          <option value="popularity">Most Popular</option>
        </select>
      </header>

      {loading ? (
        <div>Searching...</div>
      ) : (
        <SearchResults results={results} />
      )}

      <Pagination
        current={page}
        onChange={setPage}
      />
    </div>
  );
}

实战案例

案例1:文件管理器

jsx
function FileManager() {
  return (
    <Routes>
      <Route path="/files" element={<FileManagerLayout />}>
        {/* 根目录 */}
        <Route index element={<FilesRoot />} />
        
        {/* 文件夹路径 - 支持任意深度 */}
        <Route path="*" element={<FolderView />} />
      </Route>
    </Routes>
  );
}

function FolderView() {
  const location = useLocation();
  const navigate = useNavigate();
  
  // 从路径解析文件夹层级
  const folderPath = location.pathname.replace('/files/', '') || '';
  const pathSegments = folderPath ? folderPath.split('/') : [];
  
  const [files, setFiles] = useState([]);
  const [folders, setFolders] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchFolderContents(folderPath)
      .then(({ files, folders }) => {
        setFiles(files);
        setFolders(folders);
      })
      .finally(() => setLoading(false));
  }, [folderPath]);

  const navigateToFolder = (folderName) => {
    const newPath = folderPath ? `${folderPath}/${folderName}` : folderName;
    navigate(`/files/${newPath}`);
  };

  const navigateUp = () => {
    if (pathSegments.length > 0) {
      const parentPath = pathSegments.slice(0, -1).join('/');
      navigate(`/files/${parentPath}`);
    }
  };

  if (loading) return <div>Loading folder contents...</div>;

  return (
    <div className="folder-view">
      {/* 路径导航 */}
      <nav className="folder-breadcrumb">
        <Link to="/files">Files</Link>
        {pathSegments.map((segment, index) => {
          const segmentPath = pathSegments.slice(0, index + 1).join('/');
          return (
            <span key={index}>
              <span className="separator"> / </span>
              <Link to={`/files/${segmentPath}`}>
                {decodeURIComponent(segment)}
              </Link>
            </span>
          );
        })}
      </nav>

      {/* 操作栏 */}
      <div className="folder-actions">
        {pathSegments.length > 0 && (
          <button onClick={navigateUp}>
            ← Back to Parent Folder
          </button>
        )}
        
        <button onClick={() => setShowUploadModal(true)}>
          Upload Files
        </button>
        
        <button onClick={() => setShowCreateFolderModal(true)}>
          New Folder
        </button>
      </div>

      {/* 文件夹列表 */}
      {folders.length > 0 && (
        <div className="folders-grid">
          <h3>Folders</h3>
          {folders.map(folder => (
            <div
              key={folder.id}
              className="folder-item"
              onClick={() => navigateToFolder(folder.name)}
            >
              <div className="folder-icon">📁</div>
              <span className="folder-name">{folder.name}</span>
              <span className="folder-size">{folder.itemCount} items</span>
            </div>
          ))}
        </div>
      )}

      {/* 文件列表 */}
      {files.length > 0 && (
        <div className="files-grid">
          <h3>Files</h3>
          {files.map(file => (
            <div key={file.id} className="file-item">
              <div className="file-icon">
                {getFileIcon(file.type)}
              </div>
              <span className="file-name">{file.name}</span>
              <span className="file-size">{formatFileSize(file.size)}</span>
              <time className="file-modified">
                {new Date(file.modifiedAt).toLocaleDateString()}
              </time>
            </div>
          ))}
        </div>
      )}

      {folders.length === 0 && files.length === 0 && (
        <div className="empty-folder">
          <p>This folder is empty</p>
        </div>
      )}
    </div>
  );
}

案例2:多语言路由

jsx
// 支持多语言的路由系统
const SUPPORTED_LOCALES = ['en', 'zh', 'ja', 'ko'];
const DEFAULT_LOCALE = 'en';

function LocalizedApp() {
  return (
    <Routes>
      {/* 默认语言路由(不带语言前缀) */}
      <Route path="/*" element={<LocalizedRoutes locale={DEFAULT_LOCALE} />} />
      
      {/* 带语言前缀的路由 */}
      {SUPPORTED_LOCALES.map(locale => (
        <Route
          key={locale}
          path={`/${locale}/*`}
          element={<LocalizedRoutes locale={locale} />}
        />
      ))}
    </Routes>
  );
}

function LocalizedRoutes({ locale }) {
  const { t } = useTranslation(locale);

  return (
    <LocaleContext.Provider value={locale}>
      <Routes>
        <Route path="/" element={<LocalizedLayout />}>
          <Route index element={<Home />} />
          
          {/* 产品路由 */}
          <Route path="products" element={<ProductsLayout />}>
            <Route index element={<ProductsList />} />
            <Route path="category/:category" element={<CategoryProducts />} />
            <Route path=":productId" element={<ProductDetail />}>
              <Route index element={<ProductOverview />} />
              <Route path="reviews" element={<ProductReviews />} />
              <Route path="specifications" element={<ProductSpecs />} />
            </Route>
          </Route>
          
          {/* 用户相关路由 */}
          <Route path="account" element={<AccountLayout />}>
            <Route index element={<AccountDashboard />} />
            <Route path="profile" element={<Profile />} />
            <Route path="orders" element={<Orders />} />
            <Route path="wishlist" element={<Wishlist />} />
          </Route>
          
          {/* 支持页面 */}
          <Route path="support" element={<SupportLayout />}>
            <Route index element={<SupportHome />} />
            <Route path="faq" element={<FAQ />} />
            <Route path="contact" element={<Contact />} />
            <Route path="tickets" element={<SupportTickets />} />
            <Route path="tickets/:ticketId" element={<TicketDetail />} />
          </Route>
        </Route>
      </Routes>
    </LocaleContext.Provider>
  );
}

function LocalizedLayout() {
  const locale = useContext(LocaleContext);
  const location = useLocation();

  // 语言切换器
  const switchLanguage = (newLocale) => {
    const currentPath = location.pathname;
    
    // 移除当前语言前缀
    const pathWithoutLocale = currentPath.startsWith(`/${locale}`)
      ? currentPath.slice(`/${locale}`.length)
      : currentPath;
    
    // 添加新语言前缀
    const newPath = newLocale === DEFAULT_LOCALE
      ? pathWithoutLocale || '/'
      : `/${newLocale}${pathWithoutLocale}`;
    
    navigate(newPath + location.search);
  };

  return (
    <div className="localized-layout" data-locale={locale}>
      <header className="layout-header">
        <nav className="main-nav">
          <Link to={locale === DEFAULT_LOCALE ? '/' : `/${locale}`}>
            Home
          </Link>
          <Link to={locale === DEFAULT_LOCALE ? '/products' : `/${locale}/products`}>
            Products
          </Link>
          <Link to={locale === DEFAULT_LOCALE ? '/account' : `/${locale}/account`}>
            Account
          </Link>
        </nav>

        <div className="language-switcher">
          <select value={locale} onChange={(e) => switchLanguage(e.target.value)}>
            <option value="en">English</option>
            <option value="zh">中文</option>
            <option value="ja">日本語</option>
            <option value="ko">한국어</option>
          </select>
        </div>
      </header>

      <main className="layout-main">
        <Outlet />
      </main>
    </div>
  );
}

// 本地化的链接组件
function LocalizedLink({ to, ...props }) {
  const locale = useContext(LocaleContext);
  
  const localizedTo = useMemo(() => {
    if (locale === DEFAULT_LOCALE) {
      return to;
    }
    
    // 确保不重复添加语言前缀
    if (to.startsWith(`/${locale}`)) {
      return to;
    }
    
    return `/${locale}${to}`;
  }, [to, locale]);

  return <Link to={localizedTo} {...props} />;
}

最佳实践总结

1. 参数处理

jsx
// 参数处理最佳实践
const parameterBestPractices = {
  // 1. 类型转换
  typeConversion: {
    good: `
      const { id } = useParams();
      const numericId = parseInt(id, 10);
      
      if (isNaN(numericId)) {
        navigate('/404', { replace: true });
        return;
      }
    `,
    explanation: '始终验证和转换参数类型'
  },

  // 2. 参数验证
  validation: {
    good: `
      const { slug } = useParams();
      
      if (!/^[a-z0-9-]+$/.test(slug)) {
        navigate('/404', { replace: true });
        return;
      }
    `,
    explanation: '验证参数格式防止注入攻击'
  },

  // 3. 默认值处理
  defaults: {
    good: `
      const page = parseInt(searchParams.get('page')) || 1;
      const limit = parseInt(searchParams.get('limit')) || 10;
    `,
    explanation: '为参数提供合理的默认值'
  },

  // 4. 错误处理
  errorHandling: {
    good: `
      useEffect(() => {
        fetchUser(userId)
          .then(setUser)
          .catch(() => navigate('/users', { replace: true }));
      }, [userId]);
    `,
    explanation: '参数无效时优雅地处理错误'
  }
};

2. 性能优化

jsx
// 参数变化优化
function OptimizedParamsComponent() {
  const { id } = useParams();
  const [searchParams] = useSearchParams();
  
  // 使用useMemo避免不必要的重新计算
  const filters = useMemo(() => {
    return {
      category: searchParams.get('category'),
      minPrice: parseFloat(searchParams.get('minPrice')) || 0,
      maxPrice: parseFloat(searchParams.get('maxPrice')) || Infinity
    };
  }, [searchParams]);

  // 防抖的参数处理
  const debouncedFilters = useDebounce(filters, 300);

  useEffect(() => {
    fetchData(id, debouncedFilters).then(setData);
  }, [id, debouncedFilters]);

  return (
    <div>
      {/* 组件内容 */}
    </div>
  );
}

// 防抖Hook
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

总结

动态路由和参数获取是构建现代Web应用的基础:

  1. 路径参数:通过useParams获取URL中的动态部分
  2. 查询参数:通过useSearchParams管理URL查询字符串
  3. 参数验证:确保参数格式正确和类型安全
  4. 状态同步:将URL参数与组件状态同步
  5. 性能优化:合理使用缓存和防抖
  6. 错误处理:优雅处理无效参数

掌握这些技术可以构建出用户友好、SEO友好的动态路由系统。