skip to Main Content

I am working on a Quiz app. Consists of a question and four options per question. I want to change the background color of either of the options when the user clicks on it i.e. when user clicks on one answer, it has a background color, when another option is selected, inside the same question and the same list of options, make the newly selected option have the color of the previous option and revert previous option back to its original color.

What is happening now is that all the questions and their options are being affected by any one question. If I click on one option under one question, the options in other questions also change. And I found out the options in each question switch position everytime I click on one option.

Anybody has a clue what’s wrong?

Thanks.

Code Sandbox link is this: https://codesandbox.io/s/pedantic-julien-dlqwk8?file=/src/App.js

Code for the Quiz component below

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);
  const [colorToggle, setColorToggle] = useState(false);
  const [colorObj, setColorObj] = useState({
    backgroundColor: "",
  });
  const [isClickedIndex, setIsClickedIndex] = useState(-1);
   

  // 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);
      });
    
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [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(index) {
    setIsClickedIndex(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(option, correctAnswer, optIndex) {

    // handleClick is called here with the value of the index of the options
    handleClick(optIndex);

    if (option === correctAnswer) {
      console.log("Correct");
      // setColorToggle(true);
     
      // Check if clicked answer exists before and reset the value back to what was clicked to eliminate same answer repeated in the userAnswer array
      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", );
      // setColorToggle(false);
    }
    
  }



  const quizElements = quiz && quiz.map(eachQuiz => {

    const incorrectOptions = eachQuiz.incorrect_answers;
    const correctOption = quiz && eachQuiz.correct_answer;
    let options = [];
    options = incorrectOptions.concat(correctOption);

    let randomOptions = createRandomOptions(options);

    return (
      <>
        <div className="quiz-wrapper">
          <p className="question">{eachQuiz.question}</p>
          <ul>
            {quiz && randomOptions.map((option, index) => 
              {
                return (
                  <li 
                    key={index} 
                    className={`option ${isClickedIndex >= 0} ? "active" : "null"`}
                    onClick={() => checkEachAnswer(option, correctOption, index)}
                    
                  >
                    {option}
                  </li>
                )
              })
            }
          </ul>
        </div>
      </>
    )
  });

  console.log(userAnswer);

  // Displays the answers when we click on the Check Again button
  function displayAnswer() {
    setShowAnswer(true);
    setPlayAgain(true);
    setShowAnswerBtn(false);
  }


  // Responsible for the Play Again button
  function updatePlayAgain() {
    setTogglePlayAgain(!togglePlayAgain);
    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>
      }
    </>
  )
}


2

Answers


  1. Chosen as BEST ANSWER

    Finally figured it out. It was because I was using the isClickedIndex as the state for tracking all of the questions' options so any change to one is reflected on the others. I also moved the callbacks in the quizElements variable into the useEffect so that I can set the values once per render and won't have to call them again as I was doing with the former code. This prevents the options from switching positions when clicked.

    Full and updated 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); // Included this when I encountered a bug whereby the useEffect was being called when I set its dependency array to the value of playAgain initially. Shout out to the nice folks on Stack Overflow.
    
      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=10&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 => {
            // Had to move this here because we only want to call the randomize function once. Doing otherwise results in bugs like the options switching position everytime we click on any.
            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, // Tracks the index of the clicked option in each question. Set to minus -1 to show that no option has been clicked yet.
              };
            });
            setQuiz(modifiedQuiz);
          })
          .catch(error => {
            console.error("An error occurred!", error);
            setQuiz(null);
            setError(true);
          })
          .finally(() => {
            setLoading(false);
          });
        
      // eslint-disable-next-line react-hooks/exhaustive-deps
      }, [togglePlayAgain])
    
    
      // Shuffles both the incorrect and correct answers
      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;
      }
    
      // Helps check for a click on our options and handles necessary functions
      function handleClick(option, correctAnswer, position, questionIndex) {
        checkEachAnswer(option, correctAnswer, position);
    
        // Checks if the index of the current question when clicked is the index of the quiz rendered initially
        const updatedQuiz = quiz.map((eachQuiz, index) =>  
          index === questionIndex 
          ? {...eachQuiz, clickedOptionIndex: position} 
          : eachQuiz);
        setQuiz(updatedQuiz);
      }
    
    
       // 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(option, correctAnswer, optIndex) {
    
    
        console.log(optIndex);
    
        if (option === correctAnswer) {
          console.log("Correct");
          // setColorToggle(true);
         
          // Check if clicked answer exists before and reset the value back to what was clicked to eliminate same answer repeated in the userAnswer array
          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", );
          // setColorToggle(false);
        }
    
        // I could try it this way or I could just set it and target the active state in the pure css file.
        // And then check to see if showAnswer is true and display a different color for those we got right or wrong.
    
        // I can move this styles variable into  outside of this function and then use an if statement to check if option is correct, then declare a variable to check if showAnswer is true, and set a color, else, we set a different color
        
      }
    
    
      const quizElements = quiz && quiz.map((eachQuiz, questionIndex) => {
    
        // Destructure each object
        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>
          </>
        )
      });
    
      console.log(userAnswer);
    
    
      // Displays the answers when we click on the Check Again button
      function displayAnswer() {
        setShowAnswer(true);
        setPlayAgain(true);
        setShowAnswerBtn(false);
      }
    
    
      // Responsible for the Play Again button
      function updatePlayAgain() {
        setTogglePlayAgain(!togglePlayAgain);
        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>
          }
        </>
      )
    }
    
    
    

  2. You can use CSS: option:checked { color: red; } and you can specify styling of selected option with the :checked tag as well.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search