ThomasM ThomasM - 2 months ago 16
React JSX Question

Make sure a new instance of certain component is used

I have a feeling this is a "wrong" question to ask, but here goes anyway:

I'm making some sort of quiz app (using redux for state management). (showing the important bits here)

quiz.js

<Slider {...sliderSettings} slideIndex={currentQuestionIndex}>
<Start onStart={() => onNextQuestion()} topicId={topicId} />

{
questions.map((question, ndx) => {
return (
<Question {...question} done={onDone} key={`question-${question.id}`} />
);
})
}

<Result score={score} onRestart={() => onRestart()}/>
</Slider>


question.js

<div className="question">
<h2 className="question__text">{ question }</h2>
<MultipleChoice options={answers} onChange={done} />
</div>


multiple-choice.js

const
initialState = {
selectedValue: null
};

class MultipleChoice extends Component {
constructor(props) {
super(props);
this.state = initialState;
}

handleChange(value, correct) {
this.setState({
selectedValue: value
});

this.props.onChange(correct);
}

render() {
const
{ options } = this.props,
getStateClass = (option, ndx) => {
let sc = '';

if (this.state.selectedValue !== null) {
if (this.state.selectedValue === ndx) {
sc = option.correct ? 'is-correct' : 'is-incorrect';
} else if (option.correct) {
sc = 'is-correct';
}
}

return sc;
};

return (
<ul className="multiple-choice">
{ options.map((option, ndx) => {
return (
<li key={`option-${ndx}`} className={cx('multiple-choice__option', getStateClass(option, ndx))}>
<button className="multiple-choice__button" onClick={() => this.handleChange(ndx, option.correct)}>{option.answer}</button>
</li>
);
}) }
</ul>
);
};
}

export default MultipleChoice;


The problem lies within the rendering of MultipleChoice. It uses internal state to show which answer is wrong and right.

in quiz.js, onRestart dispatches a redux action which updates the store to fetch some new questions and reset the currentQuestionIndex to 0. This all works.
But somehow, sometimes the MultipleChoice element is "reused" and is still showing the state it had in the previous round of questions. In other words, most of the time a new MultipleChoice gets mounted, but sometimes it isn't. This is react reconciliation, if I understand correctly?

But how do I solve this problem? In my view, MultipleChoice needs its internal state. So should I reset this state somehow? Or make sure a new MultipleChoice gets mounted everytime? Or am I asking the wrong questions here?

Answer

I looked at your repository, and the issue, as correctly noted in another answer is that your <Question> (and thus inner <MultipleChoice>) components never unmount, so they keep their state.

Normally this doesn’t come up often in React because people usually want the state to be preserved while the component is in the tree. When the state is no longer needed, people usually stop including components in the render() method, and React unmounts them. Next time they are rendered, their state gets reset.

The state does not get reset in your example because you always keep the <Question>s visible, even between the quiz runs. You can see that <Question>s are already mounted before we begin the quiz, and stay mounted after it ends:

stale-questions

So how can we force React to reset their state? There are three options:

  • You may cause them to unmount. Next time they get mounted, they’ll have a new state. This is usually the simplest solution, because you don’t actually display the questions on the initial “start quiz” page. For example, you can do this by adding currentQuestionIndex > 0 && guard before rendering questions.map(...) in the render() method of <Questions>.

  • You may pass new keys to them that don’t match previous keys. You are currently using question-${question.id} as the key right now but that will produce the same key for the same question even if you retry the quiz. To solve this, you could introduce a new state variable (either in Redux or in top-level component state), for example, quizAttemptIndex, and increment it on any new attempt. Then you could change the key to be question-${quizAttemptIndex}-${question.id}. This way attempting a quiz another time would reset the internal state of the question (as well as cause it to remount).

  • Finally, if you’d rather not destroy the DOM completely by passing a different key, you could pass quizAttemptIndex (explained in the previous section) as a prop to <MultipleChoice>. Then, inside it, you could this.setState({ selectedValue: null }) inside componentWillReceiveProps(nextProps) if nextProps.quizAttemptIndex !== this.props.quizAttemptIndex.

You can choose either solution depending on how important it is for you to keep the questions mounted all the time.

Comments