Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

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

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:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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.

>Solution :

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>
    )
  };
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading