React子组件重渲染疑问:为何QuestionTimer未随父组件更新?
场景与代码
我正在开发一款React测验应用,包含Quiz和QuestionTimer两个核心组件。当前遇到的问题是:超时跳转至下一题后,进度条和计时器并未重置。通过给QuestionTimer添加key prop强制重建组件可以解决,但我疑惑的是:父组件Quiz因userAnswers状态更新重渲染时,为什么QuestionTimer没有自动重置?
Quiz组件代码
export default function Quiz() { const [userAnswers, setUserAnswers] = useState([]); const currQuestionIdx = userAnswers.length; const quizIsComplete = currQuestionIdx === QUESTIONS.length; const handleSelectAnswer = useCallback( (answer) => { setUserAnswers((prevUserAnswers) => { return [...prevUserAnswers, answer]; }); }, [] ); const handleSkipAnswer = useCallback(() => { handleSelectAnswer(null); }, [handleSelectAnswer]); if (quizIsComplete) { return ( <div id="summary"> <img src={quizComplete} alt="Trophy Icon" /> <h2>Quiz Completed!</h2> </div> ); } return ( <div id="quiz"> <div id="question"> <QuestionTimer timeout={10000} onTimeout={handleSkipAnswer} /> {/* 其他展示选项的JSX代码 */} </div> </div> ); }
QuestionTimer组件代码
export default function QuestionTimer({ timeout, handleTimeout }) { const [remainingTime, setRemainingTime] = useState(timeout); useEffect(() => { const timer = setTimeout(handleTimeout, timeout); return () => { clearTimeout(timer); }; }, [timeout, handleTimeout]); useEffect(() => { const interval = setInterval(() => { setRemainingTime((prevRemainingTime) => { return prevRemainingTime - 10; }); }, 10); return () => { clearInterval(interval); }; }, []); return <progress id="question-time" value={remainingTime} max={timeout} />; }
问题核心原因
父组件Quiz因userAnswers更新重渲染时,QuestionTimer的props没有发生变化,React会复用现有组件实例,导致内部状态和定时器逻辑无法重置,具体拆解如下:
组件重渲染触发条件不满足:React组件只有在props、自身状态或上下文变化时才会重渲染。这里
QuestionTimer的timeout固定为10000,handleSkipAnswer通过useCallback包裹,依赖的handleSelectAnswer也因useCallback空依赖保持稳定,所以props无变化,组件不会触发重渲染。内部状态被保留:复用组件实例时,
useState初始化的remainingTime会保留上一题结束后的剩余值(比如0),不会重新初始化为timeout值。无依赖useEffect不会重新执行:第二个
useEffect的依赖数组为空,只会在组件挂载时执行一次。复用组件时不会再次触发,所以进度条的更新定时器不会重启,进度自然无法重置。
加key解决问题的原理
给QuestionTimer添加key(比如key={currQuestionIdx})后,每切换一题key值就会变化。React会判定这是全新的组件实例,销毁旧组件并挂载新组件:新组件的remainingTime会重新初始化为timeout,两个useEffect也会重新执行,计时器和进度条就正常重置了。
内容的提问来源于stack exchange,提问作者codean

