I am working on creating a quiz page that fetches data using the useEffect
hook. The data fetched include the question and the options which are multiple choice. I have a Check Answer
button that displays the final score of the user when clicked. I also have a Play Again
button that is displayed after the user's score has been rendered.
When the user clicks on the Play Again
button, it is supposed to fetch another quiz data but this happens prematurely when the Check Answer
button is pressed. What I mean is that, when the user clicks on the Check Answer
button, it displays the score (good) but then the whole page gets re-rendered prematurely so the user can't see the options they got wrong and which were right. Clicking the Play Again
button also re-renders the quiz page which was the intended behaviour. I have the playAgain
state set as the dependency array of the useEffect so that it could re-render the quiz when the state changes. But I think that's what's responsible for the problem.
Another thing - dunno if I should ask it as a different question but I'll include it here - is that when the user clicks on the right option for each question, the options get reshuffled. It's probably not a big deal but I thought I needed to fix it to improve the UX. My guess is that it has something to do with the checkEachAnswer
function but I'm at loss of what is wrong with it.
Any idea what could be wrong. Thanks.
Code for the Quiz component below.
export default function Quiz() {
const [quiz, setQuiz] = useState(null);
const [playAgain, setPlayAgain] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [userAnswer, setUserAnswer] = useState(() => []);
const [showAnswer, setShowAnswer] = useState(false);
const [showAnswerBtn, setShowAnswerBtn] = useState(true);
// This hook fetches data once
// Added error handling to prevent errors filling up the UI
useEffect(() => {
fetch("https://opentdb.com/api.php?amount=5&category=18&difficulty=hard&type=multiple")
.then(result => {
if (!result.ok) {
throw new Error("This is an HTTP error", result.status);
}
else {
return result.json();
}
})
.then(data => {
setQuiz(data.results);
console.log("Quiz data stored", data);
console.log(quiz);
return quiz && console.log("This is quiz data", quiz);
})
.catch(error => {
console.error("An error occurred!", error);
setQuiz(null);
setError(true);
})
.finally(() => {
setLoading(false);
});
}, [playAgain])
const quizElements = quiz && quiz.map(eachQuiz => {
const incorrectOptions = eachQuiz.incorrect_answers;
const correctOption = quiz && eachQuiz.correct_answer;
let options = [];
options = incorrectOptions.concat(correctOption);
function createRandomOptions(arr) {
let copyOptions = [...arr];
let randomOptionsArr = [];
while (copyOptions.length > 0) {
let randomIndex = Math.floor(Math.random() * copyOptions.length);
randomOptionsArr.push(copyOptions[randomIndex]);
copyOptions.splice(randomIndex, 1);
}
return randomOptionsArr;
}
let randomOptions = createRandomOptions(options);
return (
<>
<div className="quiz-wrapper">
<p className="question">{eachQuiz.question}</p>
<ul>
{quiz && randomOptions.map((option, index) =>
{
// Checks if the clicked option is the correct one and also checks if it was already picked before and prevents it from being added to the userAnswer array
function checkEachAnswer() {
if (option === correctOption) {
console.log("Correct");
if (userAnswer.includes(option)) {
let userAnsArrCopy = [...userAnswer];
let index = userAnsArrCopy.findIndex(elem => elem);
userAnsArrCopy[index] = option;
setUserAnswer(prevValue => {
return userAnsArrCopy;
});
}
else {
setUserAnswer(prevValue => {
return [...prevValue, option];
});
}
}
else {
console.log(option, "Is incorrect", );
}
}
return (
<li
key={option}
className="option"
onClick={() => checkEachAnswer()}
>
{option}
</li>
)
})
}
</ul>
</div>
</>
)
});
console.log(userAnswer);
function displayAnswer() {
setShowAnswer(true);
setPlayAgain(true);
setShowAnswerBtn(false);
}
function updatePlayAgain() {
setPlayAgain(false);
setShowAnswer(false);
setShowAnswerBtn(true);
setUserAnswer([]);
}
return (
<>
{loading && <h3>Currently loading...</h3>}
{error && <h3>An error occurred while fetching data!</h3>}
{quiz && <h1 className="topic">Topic: Computer Science</h1>}
{quiz && quizElements}
{showAnswer && <p>You scored {userAnswer.length} / {quiz.length}</p>}
{quiz && showAnswerBtn &&
<button
onClick={() => displayAnswer()}
className="main-btn"
>
Check Answer
</button>
}
{quiz && playAgain &&
<button
onClick={() => updatePlayAgain()}
className="main-btn"
>
Play Again
</button>
}
</>
)
}
I would also include for the App component incase the problem isn't my Quiz component.
import { useState } from "react";
import Quiz from "./components/Quiz";
const quizId = [
{"id": 0},
{"id": 1},
{"id": 2},
{"id": 3},
{"id": 4},
]
export default function App() {
const [startQuiz, setStartQuiz] = useState(false);
function loadQuiz() {
console.log(startQuiz);
setStartQuiz(true);
}
return (
<>
{!startQuiz &&
<div className="intro-container">
<div className="intro-screen">
<h1>Quiz My Heart</h1>
<button className="start-btn" onClick={loadQuiz}>
Go to Quiz
</button>
</div>
</div>}
{startQuiz &&
<main>
<Quiz key={quizId.map(myId => myId.id)}/>
</main>
}
</>
);
}
In your displayAnswer function which is called when you click Check answer button. You set play again to true, changing it which calls your useEffect into action. You can add another state(boolean) that you just toggle to invoke your useEffect. You would only update the reset in the updatePlayAgain function
your useEffect would only have reset in the dependency array