本文讲解 react 中处理异步状态更新时的常见陷阱,重点解决因 `setstate` 异步性导致的“删除后数据未刷新”问题,并提供基于函数式更新、zustand 状态管理及分页逻辑的完整解决方案。
在 React 中,useState 和 Zustand 的 set 方法均为异步批处理机制,直接读取当前 state 变量(如 pageNumber 或 recipes)再参与计算,极易引发竞态条件(race condition)。你遇到的核心问题正是典型表现:handleDelete 中调用 setPageNumber(pageNumber + 1) 时,pageNumber 是闭包捕获的旧值;而后续 getData() 仍使用该旧值请求第 1 页,导致新数据覆盖了刚删除的列表。
React 官方明确推荐:当新状态依赖前一个状态时,必须使用函数式形式,确保获取的是最新值:
// ❌ 错误:依赖闭包中可能过期的 pageNumber setPageNumber(pageNumber + 1); // ✅ 正确:函数式更新,参数 guaranteed 为最新值 setPageNumber(prev => prev + 1);
同理,Zustand 的 set 也支持函数式写法,应避免直接解构旧 state:
// ❌ 避免在 set 中直接引用外部 recipes 变量
setRecipes(updatedRecipes); // recipes 可能已过期
// ✅ 推荐:在 set 回调中读取最新 state
set(state => ({
recipes: state.recipes.filter(recipe => !selectedIds.includes(recipe.id))
}));将删除逻辑与数据加载解耦,删除后不再立即调用 getData()(它本意是“加载下一页”,而非“重载当前页”)。正确流程应为:
以下是优化后的关键代码:
const handleDelete = () => {
const selectedIds = selectedItems.map(Number);
// 使用 Zustand 函数式更新,确保基于最新 recipes
useRecipesStore.setState(state => ({
recipes: state.recipes.filter(recipe => !selectedIds.includes(recipe.id))
}));
setSelectedItems([]);
// 判断是否需要翻页:若当前页已无数据且非第一页,则加载下一页
const currentRecipes = useRecipesStore.getState().recipes;
if (currentRecipes.length === 0 && pageNumber > 1) {
setPageNumber(prev => prev + 1); // ✅ 函数式更新
}
};⚠️ 注意:getData() 不应在 handleDelete 中调用。它应严格由 useEffect 在 pageNumber 变化时触发,或由用户手动“加载更多”触发。
你原代

const url = `https://api.punkapi.com/v2/beers?page=${pageNumber}&per_page=${recipesPerPage}`;
const getData = async () => {
try {
const { data } = await axios.get(url);
// 直接替换为新页数据(非追加),避免拼接逻辑错误
useRecipesStore.setState({ recipes: data });
} catch (error) {
console.error('Failed to fetch recipes:', error);
}
};
useEffect(() => {
getData();
}, [pageNumber]); // ✅ 依赖 pageNumber,自动响应翻页若需「无限滚动」式追加,再启用 useEffect 监听 pageNumber 并追加,但删除操作不应干扰该流程。
遵循以上原则,即可彻底规避“删除无效”“页码不更新”等经典 React 异步陷阱,写出可预测、易维护的状态管理逻辑。