syd
syd

Reputation: 325

Execute code on parent component based on parent component's state changed in child component

I am learning reactjs and making a quiz app. I want to execute a piece of code in parent component using useEffect. the dependency of this useEffect function is based on a state of the parent component, which will be changed in the child component. I have sent the set method to child component. as per my understanding, the useEffect function should work in the parent component, but it's not happening. What am I doing wrong?

I am sending answers and setAnswers to child component and trying to log in the parent component that answer is updated, when setAnswers is called in the child component.

Parent Component:

import React, { useState, useEffect, useRef } from 'react'
import axios from 'axios'
import { baseURL } from '../assets/axiosInstance'
import Row from 'react-bootstrap/Row'
import QuizCard from './QuizCard'
import Spinner from 'react-bootstrap/Spinner'
import Button from 'react-bootstrap/Button'

export default function QuizFetch(props) {

    const quiz_id = props.quiz_id
    const quiz_uri = props.quiz_uri
    const [quiz, setQuiz] = useState()
    const [answers, setAnswers] = useState([])
    const [correctAnswers, setCorrectAnswers] = useState([])
    const [my_error, setError] = useState({has_error: false, message: 0})
    const [apiCalling, setApiCalling] = useState(true)
    const [ansCounter, setAnsCounter] = useState(0)

    useEffect(()=>{
        console.log('answers changed')
    },[answers])


    const first_data_effect = useRef(true)
    useEffect(() => {
        if(first_data_effect.current){
            first_data_effect.current = false
            return
        }
            let ans = []
            let q_len = quiz.questions.length
            for(let c = 0; c < q_len; c++){
                let questions = quiz.questions[c]
                let a_len = questions.options.length;
                for (let d = 0; d < a_len; d++){
                    if (questions.options[d].correct_answer === 'yes'){
                        
                        ans[`question_${questions.id}`] = questions.options[d].id
                        
                    }
                }
            }
            console.log('ans',ans)
            setCorrectAnswers(ans)
            console.log('corr ans',correctAnswers)
            setAnsCounter(quiz.questions.length)
            console.log('qlen',quiz.questions.length)

    }, [quiz])

    useEffect(() => {
        axios.post(baseURL + 'quiz/get-quiz', {
            quiz_id: quiz_id,
            quiz_uri: quiz_uri
        }, {
            headers: {
                'Accept': 'application/json',
                'Authorization': `Bearer ${localStorage.getItem('scs_token')}`
            },
        })
            .then((response) => {
                const resp = response.data
                setQuiz(resp[0])
            })
            .catch((error) => {
                console.log("inside error")
                setError({
                    has_error: true,
                    message: error.response.data['message']
                })
            })
            .then(() => {
                setApiCalling(false)
            })
    }, [])


    if (apiCalling) {
        console.log('api calling true')
        return (
            <>
                <div style={{ height: "200px", display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                    <Spinner animation="grow" variant="primary" />
                </div>
            </>
        )
    } else if (my_error['has_error']) {
        console.log('error')
        return (
            <>
                <h1>Quiz: {props.quiz_uri}</h1>
                {my_error['message']}
            </>
        )
    } else {
        return (
            <>
            <div>
                <h1 style={{ textAlign: 'center'}}>
                    {quiz.quiz_title}
                </h1>
                Yet to Answer: {ansCounter}
                Answered: {answers.length}
                <Row style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                    {quiz.questions.map(q => (
                        <QuizCard key={q.id} quizid={quiz.id} question={q} answers={answers} setAnswers={setAnswers}/>
                    ))}
                </Row>
                <br />
                </div>
                <br /><br />

            </>
        )
    }

}

Child Component:

import React, {useState, useEffect} from 'react'
import Col from 'react-bootstrap/Col'
import axios from 'axios'
import { baseURL } from '../assets/axiosInstance'
import Spinner from 'react-bootstrap/Spinner'


export default function QuizCard(props){
    const [question, setQuestion] = useState(props.question)
    const [showCorrect, setShowCorrect] = useState(false)
    // const [apiCalling, setApiCalling] = useState(false)

    const onChangeValue = (e) =>{
        setShowCorrect(true)
        let result = props.answers
        result[e.target.name] = e.target.value
        props.setAnswers(result)
        console.log('on change answers',props.answers)
        // setApiCalling(true)
        axios.post(baseURL + 'quiz/record-each-answer', {
            quiz_id: props.quizid,
            question_id: e.target.name,
            option_id: e.target.value
        }, {
            headers: {
                'Accept': 'application/json',
                'Authorization': `Bearer ${localStorage.getItem('scs_token')}`
            },
        })
            .then((response) => {
                console.log('successful')
            })
            .catch((error) => {
                console.log('failed')
            })

        
    }

    return(
        <>
        <Col md={3} xs={8} style={{margin:"5px",padding:"15px 10px", backgroundColor:"#343a40", borderRadius:'15px', color:"white", position:"relative",top:'0px'}}>
        <h5 style={{marginLeft:"10px"}}>{question.question}</h5> 
        {/* {apiCalling ? <Spinner animation="border" variant="primary" size="sm" /> : ""} */}
        <div style={{marginLeft:"10px"}}>
            {question.options.map(option=>(
                <div key={option.id}>
                    <input 
                    type="radio" 
                    disabled={showCorrect}
                    {...props.answers[`question_${question.id}`] === option.id ? "checked" : ""} 
                    value={option.id} 
                    onClick={(e)=>onChangeValue(e)} 
                    name={`question_${question.id}`}/>
                &nbsp;<span style={{width:'100%', backgroundColor:((showCorrect && option.correct_answer) === "yes" ? "green": "")}} >{option.answer}</span></div>
                
            ))}
      </div>
      
        </Col>
        
        </>
    )
}

Upvotes: 0

Views: 35

Answers (1)

trixn
trixn

Reputation: 16354

The problem is that you are mutating the state rather than creating a new object holding the new state. You should never mutate what is in the state. React compares state by identity and because you are just adding a property to the same object react thinks that the state hasn't changed because it's still the same object. You need to create a new object and merge the old state with the new answer:

const onChangeValue = (e) => {
    // ...
    props.setAnswers(currentAnswers => ({...currentAnswers, [e.target.name]: e.target.value}));
    // ...
}
   

Note how this uses the callback version of setAnswers. It accepts a function that gets the current state as the argument and should return the new state. You should always use that when you want to update the state based on the current state.

Upvotes: 1

Related Questions