Building a Simple Quiz App Using a Rest API, React, and Redux

Emily Wheatcroft
Geek Culture
Published in
10 min readMay 2, 2021

This post describes how to build a simple, single-player quiz platform. The app is published on Netlify, and the finished source code can be seen here on Github.

These are the 5 steps that we’ll go through:

  1. Settings component
  2. Setting up a simple redux store
  3. Making the questions request
  4. Adding a question component
  5. The final page

You’ll notice that I haven’t included the styling files in the article, but you can see the app css file here.

Getting started

The first thing I did was find a good API resource for the questions. After a bit of Googling I found Open Trivia Database, which has thousands of questions and is moderated. If you take a look at the API config you can see that there are all the parameters you need to create a quiz platform: number of questions, category, difficulty, type & encoding options.

Using create-react-app, I set up using the default options, opting not to add redux initially (this is something that I reassess later). Start by running these commands in your terminal:

npx create-react-app quiz-app
cd quiz-app
yarn start

Settings

A settings page for the user to customise the question request

Let’s begin by working on the settings page at src/Components/Settings.js, which is the first component that the user will see.

We’re going to use a functional component, so the first step is outlining this:

Then importing the Settings component into App.js

Next we want to create the structure of the interface using JSX. We know from the API config that we want inputs to control the question category, question difficulty, question type, and the amount of questions.

We can retrieve the question categories from this endpoint: https://opentdb.com/api_category.php. We’re going to make this request and save the categories in the component using the React state hook and effect hook, like so:

We import useEffect and useState from react, then we can declare a state variable for the options with an initial null value. This is where we will store the data from the API response using setOptions.

In a useEffect hook we declare the api url, make a request using fetch, then pass the necessary json as a payload in setOptions.

The question categories are now available in the state as options, so let’s make them selectable!

Firstly, we declare a state variable for questionCategory, with the initial value of an empty string. We will use setQuestionCategory to update this value.

Then we want to give the option of to retrieve questions from all categories, which we hard code as an option. For the rest of the options we map the array of options, making sure to assign each option a key for reactivity. We use the name as the option display, while using the id as the option value, as this is the value expected by the API.

Now we add an onChange event named handleCategoryChange, which uses setQuestionCategory to update questionCategory in the state whenever the user selects an option.

Finally, we bind the value of the select element to questionCategory.

Now we can add in a simple loading state:

The rest of the question options will have to be hard-coded as they are not available on the API endpoint. We can use the same useEffect and useState patterns, with src/Components/Settings.js growing to look like this:

Now we have all of the information we need to make the API call for the questions.

This is when I realised that it will be easiest to keep this information in a redux store. It would be possible to pass the options and questions between components, but I prefer keeping the app data in a centralised store as it allows for more flexibility and scalability.

Setting up a simple redux store

Since this is a small app, we don’t need to worry about a complex store structure — a single file for reducers should be enough.

We need to run the following to add redux and react-redux to our app:

yarn add redux react-redux

And update App.js like so:

Now we need to create src/Reducer.js and begin moving the options state from src/Components/Settings.js to the redux state.

As you can see below, we can declare the initial state as a variable. Under the options key in this variable, we can store the values currently held in the Settings component.

We also need a way to update these values. Since this is a relatively simple redux setup, we can just use a switch function in the reducer to consume the actions that will be sent from our components:

You may notice that we are using the spread operator (…) when updating the state object. By doing this we are creating a copy of the state object, which we then will add our new value to, which means that we are not mutating the state object directly. This is best practice when working with a store.

We can return to the Settings component to replace the state and effect hooks with redux actions. We’ll need to import useSelector and useDispatch from react-redux. useSelector allows us to access the state, while we can use useDispatch to update it.

You’ll notice that we haven’t had to change any of the JSX here, which is great! That’s because we’ve handled all change events neatly in functions, where we can simply change hook functions to dispatch actions, passing the action type and values as a payload.

We can look at the change within handleCategoryChange as a specific example of this. Originally it was like this:

const handleCategoryChange = event => {
setQuestionCategory(event.target.value)
}

And now:

const handleCategoryChange = event => {
dispatch({
type: 'CHANGE_CATEGORY',
value: event.target.value
})
}

We’re now defining the type of action within the payload object. We do this because the action is now interpreted by the switch function in Reducer.js:

// src/Components/Reducer.js
...
case "CHANGE_CATEGORY":
return {
...state,
options: {
...state.options,
question_category: action.value
}
}
...

So the Reducer is recognising that in this case we want to update question_category in the redux state. The value in the payload is simply the value that we want question_category updated to in the store.

You may also notice that useDispatch() is assigned to a variable like this:

const dispatch = useDispatch()

This is so that we can pass it as a dependency to useEffect(). We’re still fetching the question categories in a hook and need to dispatch an action from within it when we are updating the loading state.

Making the questions request

Now that the questions options are available in the redux store for all components to access, we can make the API request for the quiz questions and save them in the store.

We’re going to create a component that does this called FetchButton.

Here you can see the relevant information being collected from the store using useSelector. Then, when the button is clicked, we handle this information in handleQuery. We dynamically build the API url parameters depending on whether an option has been selected - if the user has left any option to “all” then we do not add it as a parameter.

In the same function, we make the API request using fetch.

Now we can handle the response and set it in the state:

We’ve added a new action, SET_QUESTION, here, so we’ll want to update the switch function in Reducer.js as well:

Adding a question component

The Question component will display the question, options and the user score.

Now we’ve got the questions saved in the state, we can create a component to display the questions and for the user to choose their answers. Since we’ll need to save the index of the question that the user is currently on, as well as the user’s current score, we can add those to the redux store with an initial value of 0. Additionally, we can add instances for the actions that update these values in the Reducer switch statement:

We can add these in our empty component at src/Components/Question.js, like so:

As well as retrieving the score, questions, and question index from the redux store, there are variables for the current question and the correct answer. useDispatch has also been imported and defined and is ready for use.

The shape of a question is as follows:

{
"category": "Entertainment: Video Games",
"type": "boolean",
"difficulty": "easy",
"question": "Peter Molyneux was the founder of Bullfrog Productions.",
"correct_answer": "True",
"incorrect_answers": [
"False"
]
}

The correct answer is returned separately from the array for incorrect answers. Therefore, I wrote a simple useEffect function to return the correct and incorrect options together, with the correct answer placed in a random position in the array. The answer options are then saved in the state as options:

The final thing to consider before adding the JSX for the Question component is that we want an “unanswered” an “answered” state. Before the user has selected an answer, the main pieces of information that we need to show are the question and the answer options. When the user has made their selection, we should show whether they have selected the correct answer, and, if they haven’t, show them what the correct answer was. Since we will be doing all of this in the same component, there is no need to send this information to the redux store, so we can control this state using hooks:

At the moment this component will display the first question and answer options, but clicking on an option does not update anything. To fix this, we add functionality to the onClick handler to the options. On click, we need to:

  • set answerSelected to true
  • set answerCorrect to true or false, depending on what was selected
  • if the answer is correct, update the score
  • if it is not the final question, move onto the next question by updating thie index

The setTimeout function with the parameter of 2500ms means that we show whether the user has got the answer correct and the correct answer for a second before moving onto the next question.

Now we need to decode the text sent from the API. As there is not a native Javascript function for this, I have implemented the solution from here — it’s not pretty, but does the job:

Finally, we can add a dynamic class to the options after the user has made their selection. I’ve used if statments so that:

  • if an answer has not been selected yet, there no list item has a dynamic class name
  • when an option has been selected, the correct answer will have the class correct
  • when an option has been selected, the list item clicked by the user will have the class selected

In App.css we can add some styling for these classes. If the list item has the class correct, then it’s background colour should be green. If a list item has the class selected and does not also have the class correct then it should have a red background. Therefore, if the user has selected the correct item it will be green. If they have selected an incorrect item then it will be red, and the correct answer will be highlighted in green:

...li:hover {
background-color: #772139;
color: white;
}
li.correct {
background-color: rgb(53, 212, 53);
}
li.selected:not(correct) {
background-color: rgb(206, 58, 58);
}
...
We can use dynamic classes to show the correct answer. Here the user has selected the correct option
Here they have chosen the incorrect option

This is the finished Question component:

The final page

We’re on the home stretch! FinalScreen is the last component we’re going to make for our quiz app.

At the moment, once the user has answered the last question there is nowhere to go — they are stuck on a page telling them whether that answer was correct or incorrect.

There are multiple ways that we could handle finishing the round of questions, but I have decided on giving the user 3 options:

  • try the same questions again
  • fetch new questions using the same settings
  • go back to the settings page
The final page should show the final score, and give the user options for what to do next.

If the user chooses to retry the same questions, we just need to set the question index and score back to 0 and they will be returned to the first question:

// src/Components/FinalScreen.js
...
const dispatch = useDispatch() const replay = () => {
dispatch({
type: 'SET_INDEX',
index: 0
})
dispatch({
type: 'SET_SCORE',
score: 0
})
}
...

If the user chooses to fetch new questions, we need to reset the index, score and make another API call for the questions. To do this, we can reuse the FetchButton component that we are already importing in Settings. Since our settings are stored in the redux store, we only need to do a couple of things here:

  • provide the button text as a prop to FetchButton
  • reset the question index and score in the handleQuery function in FetchButton if the questionIndex is greater than 0. This is because we want to go to the first question and reset the score if the user chooses to request more questions

Finally, since the Settings component is shown in App.js if there are no questions present in the redux store - if the user chooses to return to settings, we just need to empty the questions array in the store and reset the score to 0:

// src/Components/FinalScreen.js
...
const settings = () => {
dispatch({
type: 'SET_QUESTIONS',
questions: []
})
dispatch({
type: 'SET_SCORE',
score: 0
})
}
...

Here is the full FinalScreen component:

Thank you for reading

And there we have it — a small, fit for purpose quiz app.

There is definitely room for expansion with this app. There are a few things that I would like to add in the future, such as:

  • Using API’s session tokens, in addition to a platform such as firebase to store the user’s settings, questions and score history
  • Create a multiplayer option

Follow me on Medium for more content!

--

--