React Context API Quiz App中useEffect重复调用API问题排查
问题分析:React问答应用API重复调用及子组件日志顺序问题
一、渲染流程梳理(点击一次开始按钮后)
- 初始状态:
startQuiz=false,quesList=null,Quiz组件因startQuiz && <Quiz/>条件不满足,不渲染。 - 触发状态更新:点击按钮调用
setStartQuiz(!startQuiz),startQuiz变为true,触发App组件重新渲染。 - 父组件渲染阶段:
App组件函数体执行,此时quesList仍为null,startQuiz为true。- 渲染JSX:
DataContext.Provider传入{quesList: null, setQuesList},因startQuiz为true,开始渲染Quiz子组件。
- 子组件渲染阶段:
Quiz组件读取Context中的quesList(值为null),执行console.log(quesList)输出null。- 尝试访问
quesList.results[0],此时会抛出Cannot read properties of null (reading 'results')错误(代码未做空值处理)。
- useEffect执行阶段:
App组件渲染完成后,触发依赖于startQuiz的useEffect,执行fetch调用API,同时console.log(quesList)输出null(闭包捕获当前渲染周期的quesList值)。
- 数据更新触发二次渲染:
- API返回数据后,调用
setQuesList(data)更新状态,触发App组件再次重新渲染。 App组件传入更新后的quesList到Context,Quiz组件重新渲染,此时能正常读取results数据并打印日志。
- API返回数据后,调用
二、问题原因分析
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
相关产品推荐
相关产品推荐

