React JS:子组件的状态更改更新父组件

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

我会尽最大努力非常具体地说明我正在开发的应用程序以及我现在面临的问题,首先,感谢您花时间阅读并帮助我!我目前正在开发一个语言评估应用程序,为此,我正在使用 React js 和 Firestore 来存储问题。我基本上是在获取问题,将它们存储在父组件状态中,然后将问题传递给问题组件。一旦用户按下下一步按钮,我就会在会话存储上增加一个计数器,我还会跟踪用户在会话存储上的进度,并且我会访问一个不同的问题,使组件重新呈现。

这完全符合预期,但是,我现在遇到的问题是整个问题页面正在重新呈现,我在页面顶部有一个不应重新显示的进度条组件 -每次问题发生变化时都会呈现,因为他的父状态没有变化。

这是我的代码:

TestQuestionsPage 组件代码 - 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 组件 - 管理每个问题的状态变化的组件


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}
    />
  );
}

问题组成部分

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>
    )
  );
}

再次感谢您的宝贵时间!!我还附上了一张 gif 来向您展示错误。

Application Gif

我尝试了一些事情和不同的方法,但到目前为止似乎没有任何效果。

从 gif 来看,我觉得整个页面都在重新加载,但我不确定 MediumButton 组件中有什么。默认情况下,表单元素内的按钮将在单击时提交表单,您需要在按钮的 onClick 处理程序中实现 preventDefault,这是一个如何实现的示例:

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>
    )
  };