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

React Context API Quiz App中useEffect重复调用API问题排查

问题分析:React问答应用API重复调用及子组件日志顺序问题

一、渲染流程梳理(点击一次开始按钮后)

  1. 初始状态:startQuiz=false,quesList=null,Quiz组件因startQuiz && <Quiz/>条件不满足,不渲染。
  2. 触发状态更新:点击按钮调用setStartQuiz(!startQuiz),startQuiz变为true,触发App组件重新渲染。
  3. 父组件渲染阶段:
    • App组件函数体执行,此时quesList仍为null,startQuiz为true。
    • 渲染JSX:DataContext.Provider传入{quesList: null, setQuesList},因startQuiz为true,开始渲染Quiz子组件。
  4. 子组件渲染阶段:
    • Quiz组件读取Context中的quesList(值为null),执行console.log(quesList)输出null。
    • 尝试访问quesList.results[0],此时会抛出Cannot read properties of null (reading 'results')错误(代码未做空值处理)。
  5. useEffect执行阶段:
    • App组件渲染完成后,触发依赖于startQuiz的useEffect,执行fetch调用API,同时console.log(quesList)输出null(闭包捕获当前渲染周期的quesList值)。
  6. 数据更新触发二次渲染:
    • API返回数据后,调用setQuesList(data)更新状态,触发App组件再次重新渲染。
    • App组件传入更新后的quesList到Context,Quiz组件重新渲染,此时能正常读取results数据并打印日志。

二、问题原因分析

1. API被调用两次的可能原因

从代码逻辑看,useEffect仅会在startQuiz状态变化时执行一次。若出现两次API调用,大概率是以下情况:

  • 按钮重复点击:快速点击按钮导致setStartQuiz触发两次,startQuiz在false→true→false间切换,触发useEffect两次。
  • 状态意外修改:代码其他位置(如Quiz组件或Context)存在修改startQuiz的逻辑,导致状态反复变化。
  • 请求竞态未处理:第一次API请求未完成时,startQuiz再次变化,触发第二次fetch(但仅在startQuiz变化时才会发生)。

2. 子组件日志先输出的原因

React的执行顺序规则:

  • 组件渲染时,父组件先执行函数体,再渲染子组件,子组件的函数体立即执行并输出日志。
  • useEffect属于渲染完成后的副作用逻辑,会在DOM更新后才执行。
    因此Quiz组件的日志会先于App中useEffect的日志输出。

三、修复方案

1. 避免API重复调用

  • 防止重复点击:将按钮改为一次性触发(而非toggle),并添加禁用状态:
    <button 
        className="startBttn"
        onClick={()=> setStartQuiz(true)} // 直接设为true,避免切换回false
        disabled={startQuiz} // 已开始时禁用按钮
    >Start Quiz</button>
    
  • 处理请求竞态:添加AbortController取消未完成的请求:
    useEffect(()=> {
        if (!startQuiz) return; // 仅startQuiz为true时调用API
        const controller = new AbortController();
        const url = "https://opentdb.com/api.php?amount=5&difficulty=easy&type=multiple";
    
        fetch(url, { signal: controller.signal })
        .then(data => data.json())
        .then(data => setQuesList(data))
        .catch(err => {
            if (err.name !== 'AbortError') console.log(err);
        });
    
        return () => controller.abort(); // 组件卸载/依赖变化时取消请求
    },[startQuiz])
    

2. 修复子组件空值报错

在Quiz组件中添加空值判断,避免读取不存在的属性:

export default function Quiz() {
   const {quesList} = useContext(DataContext)

   if (!quesList) return <div>Loading...</div>; // 数据未加载时显示加载状态

   console.log(quesList)
   const question = quesList.results[0].question;
   console.log(question)
   const correct_answer = quesList.results[0].correct_answer;
   console.log(correct_answer)
   const incorrect_answers = quesList.results[0].incorrect_answers;
   console.log(incorrect_answers)

   return (
     <div>
         <h1>Quiz</h1>
     </div>
   )
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 05:01:19