Home > database >  React JS: State Changes on Child component updating the Parent Component
React JS: State Changes on Child component updating the Parent Component

Time:03-28

I will do my best to be very specific about the application I am developing and the issue I am facing right now, first of all, thanks for your time reading and helping me out! I am currently developing a language assessment Application, for that, I am using react js and Firestore to store the questions. I am basically fetching the questions, storing them on a parent component state and passing the questions to a Question component. Once the user presses on the next button, I have a counter on session Storage that is incremented, I also track the user's progress on session storage and I access a different question making the component to re-render.

That's working perfectly as expected, however, the issue I am having right now is that the whole question page is being re-rendered, I have a progress bar component on the top of the page that should not be re-rendered every time a question changes since his parent state is not changing.

Here is my code:

TestQuestionsPage component code - Parent

import { useEffect } from 'react';
import { ProgressBar } from '../../ProgressBar/ProgressBar';
import { QUESTION_TYPES } from '../../../utils/constants';
import QuestionStateHandler from '../../QuestionStateHandler/QuestionStateHandler';

export default function TestQuestionsPage() {
  useEffect(() => {
    console.log('re rendering');
    return () => console.log('testQuestionPage unmounting');
  }, []);

  return (
    <div>
      <ProgressBar questionsType={QUESTION_TYPES.GRAMMAR} />
      <QuestionStateHandler />
    </div>
  );
}

QuestionStateHandler Component - The component that manages state changes for each question


import React, { useEffect, useState } from 'react';
import Question from '../Question/Question';

import { queryQuestions } from '../../utils/firebase-utils';
import { encryptData } from '../../utils/crypto-utils';

export default function QuestionStateHandler() {
  const [testType, setTestType] = useState('grammarQuestions');
  const [level, setLevel] = useState('A1');

  //max questions will change based on the testType;
  const [maxQuestionsPerLevel, setMaxQuestionsPerLevel] = useState(10);

  const [setOfQuestions, setQuestions] = useState(null);

  //state variable that will hold the user score
  const [userScore, setUserScore] = useState(0);

  //total number of questions
  const [totalQuestions, setTotalQuestions] = useState(50);

  useEffect(() => {
    //if we don't have any questions
    console.log('fetching questions');
    queryQuestions(
      testType,
      ['A1', 'A2', 'B1', 'B2', 'C1'],
      maxQuestionsPerLevel,
    ).then((res) => {
      console.log(res);
      const encryptedQuestions = encryptData(res);
      setTotalQuestions(res.length);
      setQuestions(encryptedQuestions);
    });
    return () => console.log('unmounting component');
  }, [testType]);

  return (
    <Question
      totalQuestions={totalQuestions}
      testType={testType}
      setTestType={setTestType}
      setOfQuestions={setOfQuestions && setOfQuestions}
    />
  );
}

Question component

import React, { useEffect, useState } from 'react';

import { useNavigate } from 'react-router-dom';
import { useSessionStorage } from '../hooks/useSessionStorage';
import { QUESTION_TYPES } from '../../utils/constants';
import MediumButton from '../MediumButton/MediumButton';
import QuestionAttachment from '../QuestionAttachment/QuestionAttachment';
import './Question.css';

import {
  decryptQuestion,
  encryptData,
  decryptData,
} from '../../utils/crypto-utils';

export default function Question({
  totalQuestions,
  testType,
  setTestType,
  setOfQuestions,
}) {
  const [checkedOption, setCheckedOption] = useState(null);
  const [isOptionSelected, setIsOptionSelected] = useState(false);
  const [questionObj, setQuestionObj] = useState(null);
  const [questionID, setQuestionID] = useState(null);
  const [correctAnswer, setCorrectAnswer] = useState(null);
  const [counter, setCounter] = useSessionStorage('counter', 0);
  const [isLastQuestion, setLastQuestion] = useState(false);

  // const { totalQuestions, testType, setTestType } = useAppContext();

  const navigate = useNavigate();

  // this function will be used to create the user progress object and track their correct answers,
  function testTakerProgress(qID, isTheAnswerCorrect) {
    //create a new object and encrypt it
    const userProgressObj = {
      question_level: questionObj.level,
      question_id: questionID,
      has_answered_correctly: isTheAnswerCorrect,
    };

    const userProgress = sessionStorage.getItem('user');
    //if we have an user progress object already created
    if (userProgress) {
      let currentProgress = decryptData(userProgress);

      currentProgress = [userProgressObj, ...currentProgress];
      sessionStorage.setItem('user', encryptData(currentProgress));
      console.log(currentProgress);
    } else {
      //we don't have an user progress created
      const progressArray = [];
      progressArray.push(userProgressObj);
      sessionStorage.setItem('user', encryptData(progressArray));
      console.log(progressArray);
    }
  }

  useEffect(() => {
    if (setOfQuestions) {
      const q = decryptQuestion(counter, setOfQuestions);
      console.log(q);
      const qID = Object.keys(q);
      setQuestionID(...qID);
      setQuestionObj(q[qID]);
      console.log(totalQuestions);
    }

    return () => {
      setQuestionObj(null);
      setQuestionID(null);
    };
  }, [setOfQuestions]);

  useEffect(() => {
    if (isLastQuestion === true) {
      console.log('we are at the last question');
    }
  }, [isLastQuestion]);

  function handleNext() {
    //incrementing the question counter
    setCounter((prevCount) => parseInt(prevCount)   1);

    if (checkedOption === correctAnswer) {
      testTakerProgress(questionID, true);
    } else {
      testTakerProgress(questionID, false);
    }

    if (counter === totalQuestions - 1) {
      setLastQuestion(true);
    }
  }

  function handleSubmit() {
    console.log('unmounting the question component');
    //navigate to the test page, unmount the component
    navigate('/');
  }

  return (
    questionObj && (
      <div className="pageContainer">
        {testType === QUESTION_TYPES.LISTENING && (
          <div
            className={`${
              testType === QUESTION_TYPES.GRAMMAR && 'disabled'
            } questionAttachmentContainer`}
          >
            <QuestionAttachment
              questionType={testType}
              questionAttachmentTitle="title"
              questionAttachmentBody={questionObj.mediaURL}
            />
          </div>
        )}
        <div className="questionContainer">
          <h4 className="questionInstruction">{questionObj.question}</h4>
          {/* <p className="questionPrompt">{questionPrompt}</p> */}
          <form className="formContainer">
            {questionObj.options &&
              questionObj.options.map((option, index) => (
                <div
                  className={`optionContainer ${
                    checkedOption === index && 'activeLabel'
                  }`}
                  key={index}
                >
                  <input
                    id={index}
                    type="radio"
                    checked={checkedOption === index}
                    onChange={() => {
                      setIsOptionSelected(true);
                      console.log(isOptionSelected);
                      setCheckedOption(index);
                    }}
                  />
                  <label htmlFor={index}>{option}</label>
                </div>
              ))}
            <div className="buttonContainer">
              <MediumButton
                text={isLastQuestion ? 'Submit' : 'Next'}
                onClick={isLastQuestion ? handleSubmit : handleNext}
                disabled={isOptionSelected ? '' : 'disabled'}
              />
            </div>
          </form>
        </div>
      </div>
    )
  );
}

Thanks again for your time!! I also attached a gif to show you the bug.

Application Gif

I tried a couple of things and different approaches but nothing seems to have worked so far.

CodePudding user response:

From gif it seems to me like whole page is reloading, but I'm not sure what is inside MediumButton component. By default button inside form element will submit the form on click, you need to implement preventDefault in your button onClick handler, here is an example how to do it:

function App() {
  const handleClick = (e) => {
    e.preventDefault();
  }
  return (
    <div className="App">

      <form>
        <p>This is simple form</p>
        <button onClick={(e) => handleClick(e)}>hello</button>
      </form>
    </div>
    )
  };
  • Related