Delfi
Delfi

Reputation: 3

Cannot update a component while rendering a different component

I'm trying to implement a multiple choice quiz in Reactjs and TypeScript with an Express backend.

A Lesson component fetches a Lesson object from the API with a field for every multiple choice question, sets the current state to the lesson object, and then renders a MultipleChoiceQuestion component for each of them. I pass down a callback function to each of them for updating the score if the answer was correct or not. However, when I call that function I get this error:

Warning: Cannot update a component (`Lesson`) while rendering a different component (`MultipleChoiceQuestion`). To locate the bad setState() call inside `MultipleChoiceQuestion`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
MultipleChoiceQuestion@http://localhost:3000/static/js/main.chunk.js:1715:89
div
div
div
Lesson@http://localhost:3000/static/js/main.chunk.js:1473:108
Route@http://localhost:3000/static/js/vendors~main.chunk.js:34738:29
Switch@http://localhost:3000/static/js/vendors~main.chunk.js:34940:29
Router@http://localhost:3000/static/js/vendors~main.chunk.js:34373:30
BrowserRouter@http://localhost:3000/static/js/vendors~main.chunk.js:33993:35
App
Context@http://localhost:3000/static/js/main.chunk.js:2721:81 index.js:1

Here's my code with non-relevant pieces stripped away

Lesson Component


    import MultipleChoiceQuestion from './MultipleChoiceQuestion'
    
    function Lesson() {
        const [lesson, setLesson] = useState<Lesson>()
        const [score, setScore] = useState(0)
        const [maxScore, setMaxScore] = useState<number>(0)
        const [attempt, setAttempt] = useState(false)
    
        useEffect(() => {
            // fetch from api
        }, [lessonID])
    
        const getQuestionScore = (questionScore: number, questionMax: number) => {
            setScore(score + questionScore)
            setMaxScore(maxScore + questionMax)
        }
        
        return (
            <div>
                        {lesson.questions.map((q: MultipleChoiceQuestion) => {
                            const incorrect = q.incorrect
                            const correct = q.correct
                            return <MultipleChoiceQuestion 
                            attempt={attempt} key={q.question} 
                            question={q.question} scoreCallback={getQuestionScore}
                            correct={correct} incorrect={incorrect} />
                        })}
    
                        <button onClick={() => setAttempt(true)} >Test</button>
                        <h3>{score}/{maxScore}</h3>
    
                    </div>
                </div>
            </div>
        )}

MultipleChoiceQuestion Component


    function MultipleChoiceQuestion(props: {
        correct: string[],
        attempt: any, incorrect: string[],
        question: string, scoreCallback: any
    }) {
        const [hasReturnedScore, setHasReturnedScore] = useState(false)
    
        const handleChange = (e: any) => {
            setSelected(e.target.value)
        }
    
        
        if (props.attempt && !hasReturnedScore) {
            setHasReturnedScore(true)
            const score = calculateScore()
            score ? props.scoreCallback(1, 1) : props.scoreCallback(0, 1)
        }
    
        return (
            <div className="container">
                        <h2 className="title is-6">{props.question}</h2>
                                {props.correct.map((choice: any) => {
                                    return <AnswerBox key={choice} choice={choice} selected={selected} changeCallback={handleChange} />
                                })}
                                {props.incorrect.map((choice: any) => {
                                    return <AnswerBox key={choice} choice={choice} selected={selected} changeCallback={handleChange} />
                                })}
        )
    }
    
    const AnswerBox = (prop: any) => {
        return (
            <label htmlFor="question" className="column radio" >
                <input
                    id={prop.choice}
                    type="radio"
                    value={prop.choice}
                    checked={prop.selected === prop.choice}
                    onChange={prop.changeCallback}
                    name="choice" />
                {prop.choice}
            </label>
        )}

I mistakenly thought it was a clever way to pass data from a child component to the parent but now I have no clue how to solve this problem.

Upvotes: 0

Views: 783

Answers (1)

Nitesh
Nitesh

Reputation: 1550

Your this part

if (props.attempt && !hasReturnedScore) {
            setHasReturnedScore(true)
            const score = calculateScore()
            score ? props.scoreCallback(1, 1) : props.scoreCallback(0, 1)
        }

currently, it is written on render of child component (MultipleChoiceQuestion). It is wrong since it is calling a function while rendering only. You need to shift this inside some event handler (a function that gets called on some event like radio button clicked) or you shift this function inside useEffect hook.

Upvotes: 1

Related Questions