Im making a quiz app. Currently i can switch to the next question by answering (clicking a button). Also, when timer expires (10 secs for each question) it automatically switches to the next question (timer
resets to default state) regardless of button clicked or no. Above mentioned things works and its fine.
However I made a timer as a seperate child component to avoid unneccesary rerenders of parent component. The problem is that when i click a button to answer the question timer
does not reset (but it should) to its default state value (10secs). I dont know how to communicate between child (Timer) and parent component (App), because i dont wanna initialize that timer
state in parent component and put it in handleAnswerCorrectness
so when currentPage
changes i also reset timer
, because it would cause parent rerender each second and lose sense of making it a child component.
How to do it properly???
const {useState, useEffect} = React;
const questions = [
{
questionText: "What is the capital city of France",
answerOptions: [
{ id: 1, answerText: "New York", isCorrect: false },
{ id: 2, answerText: "London", isCorrect: false },
{ id: 3, answerText: "Paris", isCorrect: true },
{ id: 4, answerText: "Dublin", isCorrect: false }
]
},
{
questionText: "Who is CEO of Tesla?",
answerOptions: [
{ id: 1, answerText: "Jeff Bezos", isCorrect: false },
{ id: 2, answerText: "Elon Musk", isCorrect: true },
{ id: 3, answerText: "Bill Gates", isCorrect: false },
{ id: 4, answerText: "Tony Stark", isCorrect: false }
]
}
];
const App = () => {
const [currentPage, setCurrentPage] = useState(0);
const handleAnswerCorrectness = () => {
if (currentPage 1 < questions.length) {
setCurrentPage((prevState) => prevState 1);
}
};
return (
<div className="App">
<div>
<h3>
Question {currentPage 1}/{questions.length}
</h3>
<Timer
currentPage={currentPage}
onCurrentPageChange={setCurrentPage}
questionsLenght={questions.length}
/>
</div>
<p>{questions[currentPage].questionText}</p>
<div>
{questions[currentPage].answerOptions.map((answerOption) => (
<button key={answerOption.id} onClick={handleAnswerCorrectness}>
{answerOption.answerText}
</button>
))}
</div>
</div>
);
}
const Timer = ({
onCurrentPageChange,
currentPage,
questionsLenght
}) => {
const [timer, setTimer] = useState(10);
useEffect(() => {
const interval = setInterval(() => {
if (timer > 0) {
setTimer((prevState) => prevState - 1);
}
if (timer === 0 && currentPage 1 < questionsLenght) {
onCurrentPageChange((prevState) => prevState 1);
setTimer(10);
}
}, 1000);
return () => clearInterval(interval);
}, [timer]);
return (
<div>
<span>{timer}</span>
</div>
);
};
const root = ReactDOM.createRoot(
document.getElementById("root")
).render(<App/>);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
CodePudding user response:
It seems that the useEffect
in Timer
is both setting state timer
and reading it in dependencies array, which could cause a conflict.
Perhaps try separating it to 2 useEffect
with more simple logic, so related code will only run when needed.
One for the purpose of countdown, and reset timer
when currentPage
changes:
// Countdown effect
useEffect(() => {
setTimer(10);
const interval = setInterval(
() =>
setTimer((prevState) => {
if (prevState > 0) return prevState - 1;
return prevState;
}),
1000
);
return () => clearInterval(interval);
}, [currentPage]);
Another to handle timeout and trigger onCurrentPageChange
:
// Timeout effect
useEffect(() => {
if (timer === 0 && currentPage 1 < questionsLenght)
onCurrentPageChange((prevState) => prevState 1);
}, [timer, currentPage, questionsLenght]);
There might be other issues that needed to be addressed, but hopefully this would still help by making the logic cleaner.
Example:
const { useState, useEffect } = React;
const questions = [
{
questionText: "What is the capital city of France",
answerOptions: [
{ id: 1, answerText: "New York", isCorrect: false },
{ id: 2, answerText: "London", isCorrect: false },
{ id: 3, answerText: "Paris", isCorrect: true },
{ id: 4, answerText: "Dublin", isCorrect: false },
],
},
{
questionText: "Who is CEO of Tesla?",
answerOptions: [
{ id: 1, answerText: "Jeff Bezos", isCorrect: false },
{ id: 2, answerText: "Elon Musk", isCorrect: true },
{ id: 3, answerText: "Bill Gates", isCorrect: false },
{ id: 4, answerText: "Tony Stark", isCorrect: false },
],
},
];
const App = () => {
const [currentPage, setCurrentPage] = useState(0);
const handleAnswerCorrectness = () => {
if (currentPage 1 < questions.length) {
setCurrentPage((prevState) => prevState 1);
}
};
return (
<div className="App">
<div>
<h3>
Question {currentPage 1}/{questions.length}
</h3>
<Timer
currentPage={currentPage}
onCurrentPageChange={setCurrentPage}
questionsLenght={questions.length}
/>
</div>
<p>{questions[currentPage].questionText}</p>
<div>
{questions[currentPage].answerOptions.map((answerOption) => (
<button key={answerOption.id} onClick={handleAnswerCorrectness}>
{answerOption.answerText}
</button>
))}
</div>
</div>
);
};
const Timer = ({ onCurrentPageChange, currentPage, questionsLenght }) => {
const [timer, setTimer] = useState(10);
// Countdown effect
useEffect(() => {
setTimer(10);
const interval = setInterval(
() =>
setTimer((prevState) => {
if (prevState > 0) return prevState - 1;
return prevState;
}),
1000
);
return () => clearInterval(interval);
}, [currentPage]);
// Timeout effect
useEffect(() => {
if (timer === 0 && currentPage 1 < questionsLenght)
onCurrentPageChange((prevState) => prevState 1);
}, [timer, currentPage, questionsLenght]);
return (
<div>
<span>{timer}</span>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById("root")).render(
<App />
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>