This is a simple React + TypeScript + Storybook application, it’s a trivia quiz where the user answers a question and when all the questions are finished, they can see their score and the correct answers.

In this blog, I will go over the most important parts of the code, which you can find here.

Handling the data received from the API

The code is in api/quizz-api.ts.

The first thing to do is to create a type that handles the response returned by the API.
The Question type mirrors the JSON object retrieved from the API

export type Question = {
    category: string;
    correct_answer: string;
    difficult: string;
    incorrect_answers: string[];
    question: string;
    type: string;
}

The data returned by the API separates the correct answer from the incorrect ones, we want a type where all answers exist in the same array, so we can shuffle it (otherwise the answer will always be at the same place!) and return it to the user.

To do so we can use Intersection types to create a new type containing a new array of strings (answers) holding the answers in the way we want it.

export type QuestionExtended = Question & {answers: string[]};

To shuffle an array you can use this function

export function shuffleArray(array: any[]) {
    return [...array].sort(() => Math.random() - 0.5);
}

Now, for the function that fetches the data, and returns it in a shuffled manner

export async function getQuizzData(): Promise<QuestionExtended[]> {
    const url = 'https://opentdb.com/api.php?amount=10&difficulty=easy&type=multiple';
    const response = await fetch(url);
    const data = await response.json();
    return data.results.map((question: Question) => ({
        ...question,
        answers: shuffleArray([...question.incorrect_answers, question.correct_answer]),
    }));
};

getQuizzData() maps each element of the data returned (a Question object) into a new object having the same properties as the original but with a new property, answers, where we have all the shuffled answers to a question.

Components

Here is the list of components used in the app

  1. Button is just a normal HTML button styled using Button.css.
  2. Question is the component that displays the question received from the API.
  3. Answer is where a single answer will be displayed.
  4. QuestionCard combines both components Question and Answer to create the question card that the user sees.
  5. Result is a table showing how the user’s answers alongside the correct ones and the score.

Let’s take a look at a few components

Answer

This is the interface for the props used by the component:

export interface AnswerProps {
  children: React.ReactNode;
  disabled?: boolean;
  onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
  state?: "neutral" | "correct" | "false";
  value?: string;
}

Notice the state prop, it’s making sure that a state can only be one of the following “neutral”, “correct”, or “false” (it can also be undefined).

This prop is used to specify the background color of the answer based on whether the user answered correctly or not.

Take a look at the “answers.css” file, you will find three css classes named just like each of the three possible states.
The Answer component is built this way:

export const Answer: React.FC<AnswerProps> = ({
  children,
  state = "neutral",
  ...props
}) => {
  return (
    <button className={["answer", "size", state].join(" ")} {...props}>
      {children}
    </button>
  );
};

Pay attention to the className attribute inside the button tag, it’s a string built by combining three css classes, one of which is the state which starts as “neutral”, this is when the user hasn’t answered yet.

neutral ans

If the answer given is correct, the answer’s background becomes green

correct ans

Red if it’s incorrect

wrong ans

QuestionCard

First, define the type of an answer given by the user

export type AnswerType = {
  answer: string;
  correctAnswer: string;
  isCorrect: boolean;
  question: string;
};

This type is key in being able to figure out if the user answered correctly or not, it’s also used to show the answers alongside the correct ones at the end of the quiz.

Now, let’s take a look at the props

export type QuestionCardProps = {
  answers: string[];
  callback: (e: React.MouseEvent<HTMLButtonElement>) => void;
  question: string;
  questionNr: number;
  userAnswer: AnswerType | undefined;
  totalQuestions: number;
};
  • answers is where all the answers of each question are stored
  • callback is called when the user clicks an answer
  • question is the question
  • questionNr tracks the number of the current question
  • userAnswer is the answer provided by the user OR simply undefined in case the answer isn’t provided yet
  • totalQuestions is the total number of questions fetched from the API (set manually)

As stated before, the QuestionCard component is formed of other components.
For each answer provided by the API, it returns a div containing an Answer component, each of which is clickable.

choose_answer.png

This is how the Answer component is called inside QuestionCard

          <Answer
            state={
              userAnswer?.isCorrect === undefined
                ? "neutral"
                : userAnswer.answer === answer && userAnswer.isCorrect
                ? "correct"
                : userAnswer.answer === answer
                ? "false"
                : "neutral"
            }
            disabled={!!userAnswer}
            value={answer}
            onClick={callback}
          >
            <span dangerouslySetInnerHTML={{ __html: answer }} />
          </Answer>

The state prop is evaluated based on if the given answer is correct.

Once an answer is given the user should no longer be able to answer again, hence the code disabled={!!userAnswer} (if the current question is not yet answered !!userAnswer will be false, otherwise it will be true).

A wrong answer is given, the background turns red, and the “Next Question” button appears (more about that in the next segment)

Wrong answer

The correct answer is given, the background turns green, and the “Next Question” button appears

Correct answer

Main Application

The App function starts by defining a few states

  const [loading, setLoading] = useState(false);
  const [number, setNumber] = useState(0);
  const [questions, setQuestions] = useState<QuestionExtended[]>([]);
  const [userAnswers, setUserAnswers] = useState<AnswerType[]>([]);
  const [score, setScore] = useState(0);
  const [gameOver, setGameOver] = useState(true);
  • loading is to wait for the promise returned by getQuizzData to be handled
  • number tracks the current question number
  • questions saves the extended questions
  • userAnswers saves all the user answers
  • score is the total score of the user
  • gameOver specifies if the game is over or not

Three functions handle the logic in the application:

  1. startQuizz

Begin by setting loading to true and gameOver to false, set questions to the returned value of getQuizzData, set score to zero, userAnswers to an empty array, and number to zero. Finally set loading to false.
This function is called when the user clicks the start button, which will only be shown if gameOver is true.

This is the first thing seen by the user when launching the application

Start

  1. checkAnswer

This function checks if the answer clicked by the user is correct, it sets the score and saves the user answer in the userAnswers array.
It is called in the callback prop of the QuestionCard component.

  1. nextQuestion

Handles the navigation of the questions, it also sets gameOver to true when the final question is reached.
It is called just after the QuestionCard, in the Button component as the callback of the onClick prop.
Note that this component is displayed only if the gameOver is true, loading is false, and if the user has answered the current question.

The Result

After answering the last question, the “Show Result” button is displayed

Final answer

Clicking the button shows the final screen

Result

The correct answers are highlighted in green, the wrong answers in red.

You can start a new quiz by clicking “Start Quiz”.