skip to Main Content

I am working on a Quiz App that fetches questions and answers from an API.

What I am trying to implement is to check that under the same question, if the user clicks on an answer that is correct, increment score by 1 and if they changed their mind and selects another answer which is wrong and that is their final answer, remove 1 from the score.

I tried implementing that in the checkEachAnswer function but I am having a logic error whereby when you click on the right option, it increments, change the answer and it decrements but it does this in a way that doesn’t make sense i.e. if you have selected the right answer first and change to wrong, it decrements by 1 and when you test all questions going back and forth, at the end of the day, even though you have 4 correct answers selected, you might end up with 0 as your total score.

Code Sandbox

Quiz component code 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); // 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);
  const [active, setActive] = useState(true);
  const [score, setScore] = useState(0);
  const [buttonClickable, setButtonClickable] = 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=easy&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.
            userAnswerPerQuiz: [],
          };
        });
        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 logic
  function handleClick(option, correctAnswer, position, questionIndex, event) {
    checkEachAnswer(option, correctAnswer);
    setActive(false);
    setButtonClickable(true);

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

  console.log(score);

  // 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) {

    if (option === correctAnswer) {
      setScore(score + 1);  
    }

    else if (option !== correctAnswer && score > 0) {
      setScore(score - 1);
    }




    /* if (option === correctAnswer && !active) {
      console.log("Correct");
     
      // 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", );

      setUserAnswer(prevValue => {
        return [...prevValue, prevValue[optIndex] = option]
      })
    } */

    
  }


  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>
                  <button
                    className={
                      `option 
                      ${clickedOptionIndex === index
                        ? "active" : "" } 
                      ${showAnswer && option === correctOption
                        ? "correct" : "" }
                      ${showAnswer && option !== correctOption && active
                        ? "wrong" : "" }`
                    }
                    key={index}
                    onClick={(event) => 
                      handleClick(option, correctOption, index, questionIndex, event)}
                    disabled={!buttonClickable}
                  >
                    {option}
                    </button>
                  </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);
    setButtonClickable(false);
  }


  // Responsible for the Play Again button
  function updatePlayAgain() {
    setTogglePlayAgain(!togglePlayAgain);
    setPlayAgain(false);
    setShowAnswer(false);
    setShowAnswerBtn(true);
    setUserAnswer([]);
    setScore(0);
    setButtonClickable(true);
  }

  return (
    <>
      {loading && <h3>Currently loading...</h3>}
      {error && <h3>An error occurred while fetching data! Please check your network connection</h3>}
      {quiz && <h1 className="topic">Topic: Computer Science</h1>}
      
      {quiz && quizElements}

      {showAnswer && <p>You scored {score} / {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>
      }
    </>
  )
}

Anybody knows what could be wrong?

Thank you.

2

Answers


  1. In the example below, you can keep track of the number of times the user selected an option.

    When submitting:

    • If they have a click count of 0
      • They did not provide a response…
    • If they have a click count of 1
      • If correct, award a point
      • If wrong, deduct a point
    • If they have a click count > 1
      • If correct, do not award a point
      • If wrong, deduct a point
    const { useCallback, useEffect, useState } = React;
    
    class HTMLDecoder {
      constructor() {
        this.parser = new DOMParser();
      }
      decode(input) {
        const doc = this.parser.parseFromString(input, 'text/html');
        return doc.documentElement.textContent;
      }
    }
    
    const randomize = () => 0.5 - Math.random();
    
    const decoder = new HTMLDecoder();
    
    const defaultQuizParams = {
      amount: 5,
      category: 18, // "Science: Computers"
      difficulty: 'easy',
      type: 'multiple'
    };
    
    const fetchQuiz = (quizParams = {}) => {
      const params = { ...defaultQuizParams, ...quizParams };
      const url = new URL('https://opentdb.com/api.php');
      for (let param in params) {
        url.searchParams.append(param, params[param]);
      }
      return fetch(url.href).then(res => res.json());
    }
    
    const processQuestion = ({ correct_answer, incorrect_answers, question }) => ({
      answer: correct_answer,
      choices: [...incorrect_answers, correct_answer].sort(randomize),
      clicks: 0,
      prompt: question,
      uuid: self.crypto.randomUUID()
    });
    
    const Choice = ({ onChange, uuid, value }) => {
      return (
        <React.Fragment>
          <input type="radio" name={uuid} value={value} onChange={onChange} />
          <label>{decoder.decode(value)}</label>
        </React.Fragment>
      );
    }
    
    const Question = ({ answer, choices, clicks, onChange, prompt, uuid }) => {
      return (
        <div className="Question" data-uuid={uuid}>
          <h2>{decoder.decode(prompt)} ({clicks})</h2>
          <ol>
            {choices.map((choice) => (
              <li key={choice}>
                <Choice onChange={onChange} uuid={uuid} value={choice} />
              </li>
            ))}
          </ol>
        </div>
      );
    };
    
    const Quiz = ({ onChange, questions }) => {
      return (
        <div className="Quiz">
          {questions.map(({ answer, choices, clicks, prompt, uuid }) => {
            return (
              <Question
                key={uuid}
                answer={answer}
                choices={choices}
                clicks={clicks}
                onChange={onChange}
                prompt={prompt}
                uuid={uuid} />
            );
          })}
        </div>
      );
    }
    
    const App = () => {
      const [questions, setQuestions] = useState([]);
    
      useEffect(() => {
        fetchQuiz().then(({ results }) => {
          setQuestions(results.map(processQuestion));
        })
      }, []);
      
      const handleChange = useCallback((e) => {
        setQuestions((oldValue) => {
          const newValue = structuredClone(oldValue);
          const question = newValue.find(({ uuid }) => uuid === e.target.name);
          question.clicks++;
          return newValue;
        });
      }, []);
    
      return (
        <Quiz onChange={handleChange} questions={questions} />
      );
    };
    
    ReactDOM
      .createRoot(document.getElementById("root"))
      .render(<App />);
    <div id="root"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
    Login or Signup to reply.
  2. The solution I came up with might not be the brightest but I’ll still provide it for you in case you want to implement it.

    I added a state to store user answers.

    const [userAnswers, setUserAnswers] = useState([])
    

    In the handleClick function store the answers in the respective positions in userAnswers array.

      function handleClick(option, correctAnswer, position, questionIndex, event) {
       userAnswers[questionIndex] = option
       // rest of the code...
      }
    

    Then in displayAnswer function filter userAnswers array if it matches correct_answer property from quiz array. And setScore to whatever the length of the filtered array will be.

      function displayAnswer() {
       const matched = userAnswers.filter((el,i) => {
       return el === quiz[i].correct_answer
      })
       setScore(matched.length)
       // ...rest of the code
      }
    

    Finally in the updatePlayAgain function setUserAnswers to empty array again.

    function updatePlayAgain() {
     setUserAnswers([])
     // ...
    }
    

    Edit: And don’t forget to remove checkEachAnswer function. It’s not needed anymore.

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