Im working on this React Quiz application as part of an online class. I have two main components. A quiz component that looks like this:
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} />
... // Some other JSX code to display the answer list and handle clicking on button (not necessary for this question.
</div>
</div>
);
}
and a QuestionTimer component that looks like this
export default function QuestionTimer({ timeout, handleTimeout }) {
const [remainingTime, setRemainingTime] = useState(timeout);
useEffect(() => {
const timer = setTimeout(handleTimeout, timeout);
return () => {
clearTimeout(timer);
};
}, [timeout, handleTimeout]); // These two are props and so we need to add them in as dependencies
useEffect(() => {
const interval = setInterval(() => {
setRemainingTime((prevRemainingTime) => {
return prevRemainingTime - 10;
});
}, 10);
return () => {
clearInterval(interval);
};
}, []);
return <progress id="question-time" value={remainingTime} max={timeout} />;
}
Basically the QuestionTimer displays a progress bar and keeps track of a timer where after the timer expires, the app will move onto a new question.
There is a bug however in the code where after the timer expires and moves onto the next question, the progress bar and timer doesn't reset. The workaround was to add a key prop on that QuestionTimer component in order to have the QuizTimer be recreated/rerendered.
This part confused me because I thought that whenever a parent component gets re-rendered due a state change (In our case the Quiz component is getting re-rendered due to us updating the userAnswers state), it'll trigger all it's child components to be re-rendered. So why is it that the QuestionTimer component isnt getting rerendered and need to add a key prop to it to force it to be re-rendered.
You're right that when the parent renders, the child renders too. But when a child rerenders, it's not going to reset state or restart effects. State, by design, will persists between renders, and an effect with
[]as a dependency array only runs on the first render (and cleans up when unmounting)If you want states to reset and an effect to redo its initial run, you don't just want a rerender, you want a remount. React only remounts when one of two things happen:
<QuestionTimer>to a<FooBar>), orSince the type isn't changing (it remains a
<QuestionTimer>), the only way to remount the component is to change its key.