Why is it that sometimes React rerenders child component and sometimes it doesnt

75 Views Asked by At

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.

1

There are 1 best solutions below

2
Nicholas Tower On BEST ANSWER

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.

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:

  1. the component type changes (eg, from a <QuestionTimer> to a <FooBar>), or
  2. the key changes.

Since the type isn't changing (it remains a <QuestionTimer>), the only way to remount the component is to change its key.