
本文详解如何在 React 前端安全、可逆地实现产品分类过滤,避免因状态覆盖导致的“二次筛选失效”问题,并提供前后端协同优化建议。
本文详解如何在 react 前端安全、可逆地实现产品列表按分类筛选,避免因状态覆盖导致的“二次筛选失效”问题,并提供前后端协同优化建议。
在 React 应用中对产品列表(ProductList)进行前端分类过滤时,一个常见却隐蔽的陷阱是:直接用 setProductList(filteredProducts) 覆盖原始产品列表状态。这会导致后续筛选始终基于上一次的子集执行——例如先选 “Roll” 得到 5 个产品,再切回 “Sashimi”,系统却在仅含 Roll 的 5 条数据中查找 Sashimi,结果为空。根本原因在于:原始全量数据丢失,过滤逻辑失去基准。
✅ 正确做法:分离「源数据」与「视图数据」
应将产品数据状态拆分为两部分:
- originalProducts:只读的原始全量列表(从 Context 或 API 初始加载一次,永不修改);
- filteredProducts:仅用于渲染的派生列表(每次筛选重新计算,不修改源数据)。
以下是重构后的 Filter 组件完整实现:
import React, { useState, useContext, useMemo } from 'react';
import { ContextProductList } from '../app';
import { Dropdown } from 'primereact/dropdown';
function Filter() {
const [category, setCategory] = useState('All'); // 默认显示全部
const [originalProducts] = useContext(ContextProductList); // ✅ 只读取,不设为 setter
// 预定义分类选项(含 "Tutto" 映射为 "All")
const categoryList = [
{ label: 'Tutto', value: 'All' },
{ label: 'Roll', value: 'Roll' },
{ label: 'Sashimi', value: 'Sashimi' },
{ label: 'Uramaki', value: 'Uramaki' },
{ label: 'Bevande', value: 'Bevande' },
{ label: 'Dessert', value: 'Dessert' },
{ label: 'Ramen', value: 'Ramen' },
{ label: 'Speciali', value: 'Special' },
{ label: 'Altro', value: 'Altro' }
];
// ✅ 使用 useMemo 缓存过滤结果,提升性能 & 避免重复计算
const filteredProducts = useMemo(() => {
if (!originalProducts || originalProducts.length === 0) return [];
if (category === 'All') return originalProducts;
return originalProducts.filter(product => product.category === category);
}, [originalProducts, category]);
const handleCategoryChange = (e: { value: string }) => {
setCategory(e.value);
};
return (
<form>
<label htmlFor="categorySelect">Seleziona una categoria:</label>
<Dropdown
id="categorySelect"
value={category}
options={categoryList}
placeholder="Seleziona una categoria"
onChange={handleCategoryChange}
className="w-full md:w-20rem"
/>
{/* 可选:显示当前筛选结果数 */}
<div className="mt-2 text-sm text-gray-600">
Mostrati {filteredProducts.length} prodotti
</div>
</form>
);
}
export default Filter;⚠️ 关键注意事项
- 不要修改原始状态:ContextProductList 应只提供 originalProducts(数组),而非 [products, setProducts] —— 若必须共享 setter,请在 Context 提供方封装 resetToOriginal() 方法。
-
后端 category === "all" 处理需严格一致:你服务端代码中使用了 equalsIgnoreCase("all"),但前端传的是 "All"(首字母大写)。建议统一为小写(如 "all"),或在 Controller 中标准化:
@GetMapping("/products/{id}/{category}") public List<Product> getProductsByCategory( @PathVariable String id, @PathVariable String category) { String normalizedCategory = StringUtils.defaultString(category).toLowerCase(); return productService.getAllProductsByCategoryForUserShop(id, normalizedCategory); } -
空结果处理:当前 findByCategoryAndUserShop 返回 null 而非空集合,会引发 NPE。务必在 Repository 层确保返回 Collections.emptyList():
@Query("SELECT p FROM Product p WHERE p.userShop.id = :userShopId AND p.category = :category") List<Product> findByCategoryAndUserShop(@Param("userShopId") String id, @Param("category") String category); // Spring Data JPA 默认返回空列表,无需额外判空
? 前后端协作建议
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 少量产品( | ✅ 纯前端过滤 | 响应快、降低请求压力,适合本例中的餐厅菜单场景 |
| 海量产品或需搜索/分页 | ? 后端分页 + 过滤 | 调用 /products/{id}?category=roll&page=1&size=20,避免传输冗余数据 |
| 多租户隔离(userShop) | ✅ 已正确实现 | 所有接口均携带 userShopId,保障数据边界安全 |
通过分离源数据与视图状态、合理使用 useMemo、统一前后端参数规范,即可彻底解决“筛选一次后失效”的问题,构建健壮、可维护的产品过滤功能。










