React开发如何避免修改HTML内容时useEffect不必要触发?
我正在学习React,创建了一个展示人员列表和数量的简单组件,代码如下:
const getPeople = async (pageNumber) => { return await axios.get("https://randomuser.me/api", { params: {page: pageNumber, results: 8 }}) .then((res) => { console.log(res.data.results); return res.data; }) .catch((err) => { throw new Error("Smth went wrong..."); }); }; const People = () => { const [pageNumber, setPageNumber] = useState(1); const [error, setError] = useState(null); const [people, setPeople] = useState([]); const clickHandler = () => { setPageNumber((prev) => (prev = prev + 1)); }; useEffect(() => { async function Get() { try { const { results } = await getPeople(pageNumber); setPeople((prevState) => [...prevState, ...results]); } catch (err) { setError(err.message); } } Get(); }, [pageNumber]); return ( <div> {error && <h3>{error}</h3>} <h2>{people.length}</h2> <div className="person-grid"> {people.map(({ name, picture }) => ( <div className="person-card"> <img src={picture.large} alt={name.first} /> <p>{name.first + " " + name.last}</p> </div> ))} </div> <div className="button-wrapperr"> <button className="load-button" onClick={clickHandler}> Load more. </button> </div> </div> ); };
组件运行效果如下:
点击加载更多按钮可以获取新数据并追加到数组中,这部分功能运行正常。但我遇到了一个问题:组件首次挂载后数组中已有8条数据,当我修改HTML相关内容比如某个元素的className时,即使useEffect的依赖项pageNumber没有发生变化,useEffect也会被触发执行,导致数组被追加新数据变成16条。
我的问题是如何避免useEffect在修改HTML内容时被触发,我希望它仅在pageNumber变化时执行,而非修改元素className这类操作时触发。
备注:该问题是热重载导致的,修改内容后刷新页面运行就会恢复正常,但我不希望每次修改className都刷新页面丢失当前状态。
这个问题是开发环境热重载(HMR)的特性导致的:热重载修改组件代码后会保留组件现有state,但会重新执行所有副作用。你修改className后组件触发热重载,pageNumber状态还保留为1,useEffect检测到依赖项pageNumber的值存在,就会重新执行拉取逻辑,再次请求第1页的8条数据追加到原有数组,就出现了总条数变16的情况。这个问题仅在开发环境出现,不会影响生产环境的运行。
方案1:新增已拉取页码判断,最简单直接
在请求逻辑前判断当前页是否已经拉取过,已经拉取过就直接跳过请求,适配你当前的分页逻辑:
useEffect(() => { async function Get() { try { // 新增判断:每页8条,已拉取总条数大于等于当前页对应最大条数时跳过请求 const isPageFetched = people.length >= pageNumber * 8; if (isPageFetched) return; const { results } = await getPeople(pageNumber); setPeople((prevState) => [...prevState, ...results]); } catch (err) { setError(err.message); } } Get(); }, [pageNumber, people.length]);
方案2:单独维护已拉取页码集合,适配更复杂的场景
如果后续可能有删除单条数据、调整每页条数的需求,可以单独用state存储已经拉取过的页码,避免判断逻辑失效:
// 新增state存储已拉取的页码 const [fetchedPages, setFetchedPages] = useState(new Set()); useEffect(() => { async function Get() { try { // 当前页已经拉取过就直接跳过 if (fetchedPages.has(pageNumber)) return; const { results } = await getPeople(pageNumber); setPeople((prevState) => [...prevState, ...results]); // 拉取成功后把当前页码加入已拉取集合 setFetchedPages(prev => new Set(prev).add(pageNumber)); } catch (err) { setError(err.message); } } Get(); }, [pageNumber, fetchedPages]);
方案3:使用数据请求库,从根源避免重复请求
如果项目可以引入第三方库,直接使用SWR或者React Query这类数据请求工具,天然支持相同参数请求的缓存、去重,不需要手动写判断逻辑,还能自动处理加载、错误、分页缓存等通用需求。
内容的提问来源于stack exchange,提问作者Notch

