You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 02:20:02