Finally, the solution came to me. The issue stemmed from using isClickedIndex
as the state for monitoring all questions' options, causing any change in one option to affect the others. By moving the callbacks in the variable quizElements
within the useEffect
, I can now set the values once per render without needing to call them repeatedly like before. This adjustment prevents the options from swapping positions when clicked.
Updated and complete code:
import { useEffect, useState } from "react"
export default function Quiz() {
const [quiz, setQuiz] = useState(null);
const [playAgain, setPlayAgain] = useState(false);
const [togglePlayAgain, setTogglePlayAgain] = useState(playAgain);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [userAnswer, setUserAnswer] = useState(() => []);
const [showAnswer, setShowAnswer] = useState(false);
const [showAnswerBtn, setShowAnswerBtn] = useState(true);
// Fetches data only once
useEffect(() => {
fetch("https://opentdb.com/api.php?amount=10&category=18&difficulty=hard&type=multiple")
.then(result => {
if (!result.ok) {
throw new Error("HTTP error occurred", result.status);
}
else {
return result.json();
}
})
.then(data => {
const modifiedQuiz = data.results.map(eachQuiz => {
const incorrectOptions = eachQuiz.incorrect_answers;
const correctOption = eachQuiz.correct_answer;
const options = incorrectOptions.concat(correctOption);
const randomOptions = createRandomOptions(options);
return {
...eachQuiz,
options: randomOptions,
correctOption: correctOption,
clickedOptionIndex: -1,
};
});
setQuiz(modifiedQuiz);
})
.catch(error => {
console.error("Error occurred!", error);
setQuiz(null);
setError(true);
})
.finally(() => {
setLoading(false);
});
}, [togglePlayAgain])
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;
}
function handleClick(option, correctAnswer, position, questionIndex) {
checkEachAnswer(option, correctAnswer, position);
const updatedQuiz = quiz.map((eachQuiz, index) =>
index === questionIndex
? {...eachQuiz, clickedOptionIndex: position}
: eachQuiz);
setQuiz(updatedQuiz);
}
function checkEachAnswer(option, correctAnswer, optIndex) {
if (option === correctAnswer) {
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];
});
}
}
}
const quizElements = quiz && quiz.map((eachQuiz, questionIndex) => {
const {question, options, correctOption, clickedOptionIndex} = eachQuiz;
return (
<>
<div className="quiz-wrapper">
<p className="question">{question}</p>
<ul>
{quiz && options.map((option, index) =>
{
return (
<li
className={
`option
${clickedOptionIndex === index
? "active" : null }
${showAnswer && option === correctOption ? "correct" : ""}
${showAnswer && option !== correctOption ? "wrong" : ""}`
}
key={index}
onClick={() =>
handleClick(option, correctOption, index, questionIndex)}
>
{option}
</li>
)
})
}
</ul>
</div>
<div className="divider"></div>
</>
)
});
function displayAnswer() {
setShowAnswer(true);
setPlayAgain(true);
setShowAnswerBtn(false);
}
function updatePlayAgain() {
setTogglePlayAgain(!togglePlayAgain);
setPlayAgain(false);
setShowAnswer(false);
setShowAnswerBtn(true);
setUserAnswer([]);
}
return (
<>
{loading && <h3>Loading...</h3>}
{error && <h3>An error occurred during data retrieval</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>
}
</>
)
}