如何优化API调用速度并解决useEffect中状态未就绪的映射错误?
问题解决与优化方案
一、解决"Cannot read properties of null (reading 'map')"错误
这个错误的核心是:API请求未完成时,questions还是初始的null状态,快速点击按钮刚好触发了此时的map调用,导致报错。可以通过以下几种方式解决:
1. 调整初始状态为数组
把questions的初始值从null改为空数组[],这样即使没有数据,map方法也能安全执行(空数组调用map不会报错):
const [questions, setQuestions] = React.useState([])
2. 点击时增加状态判断
在handleClick里先检查questions是否存在,再执行map:
function handleClick() { setStartGame(false) // 确保questions有值再执行map if (questions) { setQuestionElements(questions.map(question => <Question key={question.id} data={question} />)) } }
3. 推荐:直接在渲染阶段处理(避免存JSX到state)
React不建议把JSX元素存在state中,更好的做法是直接根据questions和startGame的状态在渲染时生成元素,同时自然处理空值情况:
// 移除questionElements这个state,直接在return里渲染 return ( <> {!startGame && questions && questions.map(question => ( <Question key={question.id} data={question} /> ))} <button onClick={handleClick}>开始测验</button> </> )
二、优化API调用速度与状态设置即时性
1. 避免重复发起API请求
原代码的useEffect没有依赖数组,会在组件每次重渲染时重复调用fetch,浪费资源。添加空依赖数组,让请求只在组件挂载时执行一次:
React.useEffect(() => { fetch("https://opentdb.com/api.php?amount=5&type=multiple") .then(res => res.json()) .then(data => setQuestions(data.results)) }, []) // 空数组确保只执行一次
2. 添加请求缓存
用localStorage缓存请求结果,下次加载时先读取缓存展示,再后台更新最新数据,提升用户感知速度:
React.useEffect(() => { // 先读缓存 const cached = localStorage.getItem('quiz_questions') if (cached) { setQuestions(JSON.parse(cached)) } // 后台请求最新数据 fetch("https://opentdb.com/api.php?amount=5&type=multiple") .then(res => res.json()) .then(data => { setQuestions(data.results) localStorage.setItem('quiz_questions', JSON.stringify(data.results)) }) }, [])
3. 增加加载状态,防止无效点击
添加isLoading状态,在请求未完成时禁用按钮并显示加载提示,既给用户反馈,又避免加载时的错误点击:
const [questions, setQuestions] = React.useState([]) const [isLoading, setIsLoading] = React.useState(true) const [startGame, setStartGame] = React.useState(true) React.useEffect(() => { fetch("https://opentdb.com/api.php?amount=5&type=multiple") .then(res => res.json()) .then(data => { setQuestions(data.results) setIsLoading(false) }) .catch(err => { console.error('请求失败:', err) setIsLoading(false) }) }, []) function handleClick() { if (!isLoading) { setStartGame(false) } } return ( <> {isLoading ? ( <p>正在加载题目...</p> ) : ( <> {!startGame && questions.map(question => ( <Question key={question.id} data={question} /> ))} <button onClick={handleClick} disabled={isLoading}>开始测验</button> </> )} </> )
4. 理解React状态更新的异步性
React的setState是异步操作,不要在调用setState后立即依赖新的state值。如果需要在状态更新后执行逻辑,应该用useEffect监听状态变化:
// 比如监听questions变化,自动处理元素生成 React.useEffect(() => { if (questions.length > 0) { // 这里可以处理questions更新后的逻辑 } }, [questions])
内容的提问来源于stack exchange,提问作者Abhishek Ghanekar
相关产品推荐
相关产品推荐

