React useEffect场景下exhaustive-deps规则提示是否正确?
现有写法的潜在问题
你现在的实现不是ESLint误报,确实存在可复现的闭包陈旧值bug,最典型的场景:
- 当用户从A的个人主页快速跳转到B的个人主页时,effect触发时捕获的
postedResources还是A用户主页的已加载状态(非null),此时B用户的首屏加载不会触发全页加载动画,页面会出现一段无任何提示的空白期,直到接口返回内容才会渲染。 - 如果后续给列表加筛选、排序、标签过滤等会重置列表的功能,只要触发重置时
profileUser和postedResourcesCurrentPage两个依赖没有变化,effect里读到的永远是旧的postedResources值,会出现加载态不显示的问题。
你提到把postedResources加入依赖会触发无限循环,这个问题的根源不是规则要求错了,而是你的状态更新逻辑和触发条件没有对齐:effect每次执行完都会调用setPostedResources传入接口返回的全新数组引用,即使内容完全一致,引用变化也会触发effect重跑,重跑又会再次触发状态更新,形成死循环。
修复方案
根据你的业务场景(分页展示用户发布的资源),有两种成熟的修复方式,不需要禁用ESLint规则就能解决问题:
方案1:逻辑简化(推荐,适合无限滚动/无跨页跳转的场景)
你判断是否显示全页加载态的核心逻辑,本质是识别「当前是不是当前用户的第一页加载」,完全不需要依赖postedResources状态,直接基于页码判断即可,从根源上避开闭包和循环问题:
// 切换用户时重置分页状态 useEffect(() => { setPostedResources(null); setPostedResourcesCurrentPage(1); setPostedResourcesTotalCount(0); setPostedResourcesPageCount(0); }, [profileUser]); useEffect(() => { async function loadPostedResources() { setPostedResourcesLoadingError(false); // 页码为1即首屏加载,显示全页loading;翻页加载不触发全页loading if (postedResourcesCurrentPage === 1) { setPostedResourcesLoading(true); } try { const response = await TutHubApi.fetchResourcesPostedByUser(profileUser._id, postedResourcesCurrentPage); // 分页场景用函数式更新拼接数据,避免全量替换丢失已加载的前页内容 setPostedResources(prev => postedResourcesCurrentPage === 1 ? response.resources : [...(prev || []), ...response.resources] ); setPostedResourcesTotalCount(response.totalCount); setPostedResourcesPageCount(response.pageCount); } catch (error) { console.error(error); setPostedResourcesLoadingError(true); } finally { setPostedResourcesLoading(false); } } loadPostedResources(); }, [profileUser, postedResourcesCurrentPage]);
这个写法逻辑更直观,没有额外的状态同步成本,只要保证切换用户时重置页码和列表状态,就不会出现问题。
方案2:Ref同步值(适合有跳页、筛选等复杂列表逻辑的场景)
如果你确实需要读取最新的postedResources值做复杂判断,又不想把它加入effect依赖,可以用useRef同步最新的状态值,ref的变化不会触发重渲染,也不需要加入依赖数组,同时能避免闭包拿旧值的问题:
const [postedResources, setPostedResources] = useState<Resource[] | null>(null); // 用ref同步列表的最新值 const postedResourcesRef = useRef(postedResources); useEffect(() => { postedResourcesRef.current = postedResources; }, [postedResources]); // 切换用户时重置分页状态 useEffect(() => { setPostedResources(null); setPostedResourcesCurrentPage(1); setPostedResourcesTotalCount(0); setPostedResourcesPageCount(0); }, [profileUser]); useEffect(() => { async function loadPostedResources() { setPostedResourcesLoadingError(false); // 从ref读取最新的列表状态,不会有闭包陈旧问题 if (!postedResourcesRef.current) { setPostedResourcesLoading(true); } try { const response = await TutHubApi.fetchResourcesPostedByUser(profileUser._id, postedResourcesCurrentPage); setPostedResources(prev => postedResourcesCurrentPage === 1 ? response.resources : [...(prev || []), ...response.resources] ); setPostedResourcesTotalCount(response.totalCount); setPostedResourcesPageCount(response.pageCount); } catch (error) { console.error(error); setPostedResourcesLoadingError(true); } finally { setPostedResourcesLoading(false); } } loadPostedResources(); }, [profileUser, postedResourcesCurrentPage]);
注意:不要直接加注释禁用
exhaustive-deps规则,绝大多数场景下这个规则的报错都是在提示你代码里存在闭包风险,直接禁用只是掩盖问题,后续迭代很容易出现状态不一致的隐蔽bug。
内容的提问来源于stack exchange,提问作者Florian Walther
相关产品推荐
相关产品推荐

