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

React项目初始渲染重复加载6条Job数据问题排查

问题分析与解决方案:初始加载Hacker News职位数据重复12条

问题原因

  1. StrictMode触发重复请求:React 18开发环境的StrictMode会自动执行两次挂载阶段的useEffect,用于检测副作用问题。你的第一个useEffect(调用fetchJobId)会被执行两次,每次都获取职位ID列表并更新状态。由于第二个useEffect依赖jobId,每次jobId更新都会触发fetchJobData,而此时pages仍为0,导致两次加载第0-6条数据,最终jobData出现12条重复内容。

  2. hasMore状态判断逻辑错误:在fetchJobData中,你使用jobData.length判断是否还有更多数据,但setJobData是异步更新的,此时jobData仍是更新前的旧值,导致判断逻辑失效。正确的判断依据应该是总职位ID数量(jobId.length),而非已加载的数据长度。

修复方案

1. 避免fetchJobId重复执行

使用useRef标记是否已加载过职位ID列表,防止StrictMode下的重复请求:

const isJobIdLoaded = useRef(false);

useEffect(() => {
  if (!isJobIdLoaded.current) {
    fetchJobId();
    isJobIdLoaded.current = true;
  }
}, []);

2. 修正hasMore判断逻辑

基于总职位ID数量判断是否还有更多数据:

const fetchJobData = async () => {
  // ... 其他代码不变
  setHasMore((pages + 1) * No_Of_Pages < jobId.length);
};

3. 优化第二个useEffect的触发条件

确保仅在初始加载(pages=0且jobData为空)或pages变化时执行fetchJobData:

useEffect(() => {
  if (jobId.length > 0) {
    if ((pages === 0 && jobData.length === 0) || pages > 0) {
      fetchJobData();
    }
  }
}, [pages, jobId, jobData]);

最终修复后的完整代码

import { useEffect, useState, useRef } from "react";
import "./App.css";

function App() {
  const [jobId, setJobId] = useState([]);
  const [jobData, setJobData] = useState([]);
  const [pages, SetPages] = useState(0);
  const [hasMore, setHasMore] = useState(true);   
  const [loading, setLoading] = useState(false);
  const No_Of_Pages = 6;
  const isJobIdLoaded = useRef(false);

  useEffect(() => {
    if (!isJobIdLoaded.current) {
      fetchJobId();
      isJobIdLoaded.current = true;
    }
  }, []);

  useEffect(() => {
    if (jobId.length > 0) {
      if ((pages === 0 && jobData.length === 0) || pages > 0) {
        fetchJobData();
      }
    }
  }, [pages, jobId, jobData]);

  const fetchJobId = async () => {
    const response = await fetch(
      "https://hacker-news.firebaseio.com/v0/jobstories.json"
    );
    const data = await response.json();
    setJobId(data);
  };

  const fetchJobData = async () => {
    setLoading(true);
    const JobrangeId = jobId.slice(
      pages * No_Of_Pages,
      (pages + 1) * No_Of_Pages
    );
    const JobDataPromise = JobrangeId.map(async (id) => {
      const response = await fetch(
        `https://hacker-news.firebaseio.com/v0/item/${id}.json`
      );
      const data = await response.json();
      return data;
    });
    const jobInfo = await Promise.all(JobDataPromise);
    setJobData((prevjob) => [...prevjob, ...jobInfo]);
    setHasMore((pages + 1) * No_Of_Pages < jobId.length);
    setLoading(false);
  };

  return (
    <div className="">
      <div className="h-[500px] w-[100%]  relative p-4 bg-gray-50 overflow-y-scroll">
        <div className="flex flex-col w-[100%] h-[100%] items-center ">
          <p className="text-orange-400 font-bold text-2xl">
            Hacker News Jobs Board
          </p>
          {jobData.length > 0
            ? jobData.map((ele) => (
                <div
                  className="w-5/6 h-16 border-[1.5px] border-gray-300 m-3 p-1.5 bg-white"
                  key={ele.id}
                >
                  <div className="flex flex-col ">
                    <div>
                      <p className="font-bold text-black text-xl">
                        {ele?.title}
                      </p>
                    </div>
                    <div className="flex justify-start gap-2">
                      <p className="font-normal text-base">By {ele?.by}</p>
                      <p>-</p>
                      <p>{new Date(ele?.time * 1000).toLocaleString()}</p>
                    </div>
                  </div>
                </div>
              ))
            : null}
          <div className="w-5/6 flex justify-start">
            <button
              className="p-3 bg-orange-400 text-white rounded-md"
              onClick={() => SetPages(pages + 1)}
              disabled={!hasMore || loading}
            >
              {loading ? "Loading..." : "Load more"}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

export default App;

额外优化点:

  • 将列表项key从index改为ele.id,避免数据顺序变化导致的渲染异常
  • 把时间戳转换为可读格式
  • 按钮禁用条件添加loading状态,防止重复点击

内容的提问来源于stack exchange,提问作者aman

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 15:02:04