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