如何在React数据获取时解决exhaustive-deps警告且避免无限重渲染
问题解决思路:useReducer+useEffect分页的依赖问题与StrictMode处理
一、为什么加listings会无限循环?
你遇到的问题本质是:useEffect的依赖数组里包含了会被自身更新的状态。当你把listings加进去后,每次请求成功更新listings,都会触发useEffect重新执行,再次发起请求,再次更新listings,形成死循环。
二、不用react-query的合理解决方法
方法1:用reducer的函数式更新避免依赖listings
核心是让reducer自己处理状态的合并,而不是在effect里依赖外部的listings。比如你的reducer逻辑可以改成:
const reducer = (prevState, action) => { switch(action.type) { case 'FETCH_SUCCESS': // 用prevState获取最新的列表,拼接新数据 return { ...prevState, listings: [...prevState.listings, ...action.payload], isLoading: false }; // 其他case... } };
这样你的useEffect只需要依赖page(分页触发变量),不需要依赖listings:
useEffect(() => { const fetchListings = async () => { dispatch({ type: 'FETCH_START' }); try { const res = await fetch(`https://hn.algolia.com/api/v1/search?tags=job&page=${page}`); const data = await res.json(); dispatch({ type: 'FETCH_SUCCESS', payload: data.hits }); } catch (err) { dispatch({ type: 'FETCH_ERROR', payload: err.message }); } }; fetchListings(); }, [page, dispatch]);
这里dispatch是稳定的(来自useReducer,组件每次渲染都返回同一个引用),所以可以安全放进依赖数组,不会触发额外重渲染。
方法2:用useRef保存最新listings引用
如果你的逻辑必须在effect里访问listings,可以用useRef同步最新状态:
const listingsRef = useRef(listings); useEffect(() => { listingsRef.current = listings; // 每次listings更新时同步ref }, [listings]); useEffect(() => { const fetchListings = async () => { dispatch({ type: 'FETCH_START' }); try { const res = await fetch(`https://hn.algolia.com/api/v1/search?tags=job&page=${page}`); const data = await res.json(); // 用ref.current获取最新的listings dispatch({ type: 'FETCH_SUCCESS', payload: [...listingsRef.current, ...data.hits] }); } catch (err) { dispatch({ type: 'FETCH_ERROR', payload: err.message }); } }; fetchListings(); }, [page, dispatch]); // 这里不需要依赖listings,只依赖page和dispatch
这种方法适合复杂场景,但尽量优先用方法1,更符合reducer的设计思路。
三、关于StrictMode双渲染的abortController处理
这个处理是完全合理的,不是“存疑”的点:
- React StrictMode在开发环境会双运行
useEffect(以及其他生命周期),目的是检测潜在的bug(比如未清理的副作用)。 - 第一次effect发起的请求还没完成,第二次effect又触发,此时用
abortController取消第一次请求,能避免重复更新状态,也符合React的设计要求。
正确的实现方式:
useEffect(() => { const controller = new AbortController(); const signal = controller.signal; const fetchListings = async () => { dispatch({ type: 'FETCH_START' }); try { const res = await fetch(`https://hn.algolia.com/api/v1/search?tags=job&page=${page}`, { signal }); const data = await res.json(); dispatch({ type: 'FETCH_SUCCESS', payload: data.hits }); } catch (err) { if (err.name !== 'AbortError') { // 忽略取消请求的错误 dispatch({ type: 'FETCH_ERROR', payload: err.message }); } } }; fetchListings(); return () => controller.abort(); // 组件卸载或effect重新执行时取消请求 }, [page, dispatch]);
四、react-query实现分页的示例代码
对比原生实现,react-query的useInfiniteQuery能自动处理分页、缓存、重复请求取消等问题:
import { useInfiniteQuery } from '@tanstack/react-query'; const fetchJobListings = async ({ pageParam = 0 }) => { const res = await fetch(`https://hn.algolia.com/api/v1/search?tags=job&page=${pageParam}`); return res.json(); }; const JobBoard = () => { const { data, fetchNextPage, hasNextPage, isLoading, isError } = useInfiniteQuery({ queryKey: ['jobListings'], queryFn: fetchJobListings, getNextPageParam: (lastPage, allPages) => lastPage.page + 1, }); if (isLoading) return <div>加载中...</div>; if (isError) return <div>加载失败</div>; return ( <div> {data.pages.map((page, idx) => ( <div key={idx}> {page.hits.map(job => ( <div key={job.objectID}>{job.title}</div> ))} </div> ))} {hasNextPage && <button onClick={() => fetchNextPage()}>加载更多</button>} </div> ); };
内容的提问来源于stack exchange,提问作者D2024
相关产品推荐
相关产品推荐

