
Nested Conditional Rendering
We have implemented our first example of a dynamic app with conditional rendering, but there is a lot more we can do. In the preceding exercise, we just have a single state and a single portion of the code that is conditionally rendered, but what if we wanted to expand that further or have even more complex application states that we want to be represented in the UI?
Conditional rendering is very useful because we can actually use it at multiple nesting levels to have an app structure that is entirely dynamic.
In the next exercise, we are going to build out a quiz app where different questions will appear depending on your answers. We will be recording the state and modifying it as the user answers each question along the way.
Exercise 3.02: Building a Conditional Quiz App
Now we need to think about how we want to build up this quiz app for the user. We will display each question and the choices for each; when the correct answer is chosen, we will display a message to the user indicating that they chose correctly, and when they pick the wrong choice, we will show a message stating that they chose incorrectly.
We will have a list of questions that we are going to use for our quiz show. To do that, we will need to set up a data structure for those questions in the following format:
- question: This is a string that will be presented to the user.
- possibleAnswers: This will be an array of strings. These are the two possible choices we will present to the user for them to choose from.
- rightAnswer: This represents the correct answer for the user.
- playerChoice: This represents the answer the player chose.
- Start off by creating our app. Let's name it exercise2:
$ npx create-react-app exercise2
$ cd ./exercise2
$ yarn start
- Delete src/logo.svg since we won't need it.
- Clear out the contents of src/App.css (but keep the file). We will get back to this later.
- Remove the import for src/logo.svg since we removed that, and add the { Component} import to the React import at the top:
import React, { Component } from 'react';
import './App.css';
- Replace the App component with a class component. This can just include a header with the name Quiz Show instead:
class App extends Component {
render() {
return (
<p className="App">
<h1>Quiz Show!</h1>
</p>
);
}
}
- Let's build our initial state with the list of questions with the appropriate answers first:
App.js
13 {
14 question: "What animal barks?",
15 possibleAnswers: ["Dog", "Cat"],
16 rightAnswer: "Dog",
17 playerChoice: null
18 },
19 {
20 question: "What animal is more closely related to a tiger?",
21 possibleAnswers: ["Dog", "Cat"],
22 rightAnswer: "Cat",
23 playerChoice: null
24 },
The complete code can be found here: https://packt.live/3fCGJA4
We start off with the constructor, passing in the props and handing those off in a call to super(). Next, we set up an initial state where we keep track of the player's score (how many questions they have answered correctly) and initialize it to zero.
- Add a function to display the questions to the user. Start by writing a utility function to display a single question; it should take an index to the right question in our state as its only argument:
displayQuestion(index) {
const question = this.state.questions[index];
return (
<p className="question-display">
<p className="question">
{question.question}
</p>
<br />
<button className="question-choice">
{question.possibleAnswers[0]}
</button>
<button className="question-choice">
{question.possibleAnswers[1]}
</button>
<br />
<p className="result-correct">
Your answer is correct!
</p>
<p className="result-incorrect">
Your answer is incorrect!
</p>
</p>
);
}
We try to keep each of the portions of the display separate, complete with distinct CSS classes, to make it easier to change the look and feel later without making major modifications to the structure. It's easier to scope out the component by verifying everything displays correctly before moving on to the phase of separating the displays out depending on how the user answers.
- Let's put the calls to displayQuestion() into our render function next. For now, we will just have four calls to displayQuestion in our render function, each passing in a different index:
render() {
return (
<p className="App">
<h1>Quiz Show!</h1>
<hr/>
{this.displayQuestion(0)}
{this.displayQuestion(1)}
{this.displayQuestion(2)}
{this.displayQuestion(3)}
</p>
);
}
This should give us our initial quiz show display, as in the following screenshot, after the browser refreshes:
Figure 3.6: Quiz show app
- Clean up the style of the page a little bit to make the elements more easily distinguishable from each other.
- Open src/App.css and let's start adding classes for each of the elements.
Each question display should have its own background and be separate from each other question display. The questions themselves should stand out and be bold, and the buttons should be made larger and easier to interact with. Finally, the right and wrong answers should have appropriately matching color schemes. We will use green and red colors here for these elements. You can use the following style sheet as the contents for src/App.css or instead choose your own design:
.question-display { background: #ddd; border: 2px solid #ccc;
margin: 20px; padding: 10px; }
.question { font-weight: bold; }
.question-choice { height: 5em; width: 20em; margin: 10px; }
.result-correct { color: #272; }
.result-incorrect { color: #922; }
This should give us a much cleaner UI, as per the following screenshot:
Figure 3.7: Quiz show app with the CSS
- Start off by writing out an answerQuestion utility function to answer each question, which will take an index and the answer chosen as its arguments.
In this function, we should find the question that the user answered, update the playerChoice property of that question, update the question list in the state, and then update the cached player score as a callback to update the state's question list:
answerQuestion(index, choice) {
const answeredQuestion = this.state.questions[index];
answeredQuestion.playerChoice = choice;
const allQuestions = this.state.questions;
allQuestions[index] = answeredQuestion;
this.setState({
questions: allQuestions
}, () => {
this.updatePlayerScore();
});
}
Note
We need to use a callback here where we recalculate a player's score per question, otherwise, if we just try to increment/decrement based on the player's response, they could click the same answer multiple times and rack up an infinite number of points.
We have not written our updatePlayerScore function yet, but we will do that soon.
- Add a call to answerQuestion to the buttons as well. Back in displayQuestion(), we will update the button displays to the following code:
<button className="question-choice" onClick={() => this.answerQuestion(index, question.possibleAnswers[0])}>
{question.possibleAnswers[0]}
</button>
<button className="question-choice" onClick={() => this.answerQuestion(index, question.possibleAnswers[1])}>
{question.possibleAnswers[1]}
</button>
- Write that updatePlayerScore function.
This should just filter the list of questions down to the questions that have been answered correctly and assign a point for each. So, for the player score, we can just set that to the length of the correctly answered questions.
To ensure the logic is working correctly, use a console.log statement and ensure that the score matches the number of correctly answered questions:
updatePlayerScore() {
const playerScore = this.state.questions.filter(q => q.rightAnswer === q.playerChoice).length;
this.setState({ playerScore });
console.log("New player score:", playerScore);
}
If we play around with this a little bit, we should be able to get points for correctly answered questions, lose points for incorrectly answered questions, and not somehow end up with more than four answered questions. The output is as follows:
Figure 3.8: Console of the Quiz show app
- Extract the portion of the template that displays the right/wrong answer result into its own helper function, which we will call displayResult.
It should take the index of the question as the only argument. If the player has not answered yet (basically, if the playerChoice property is null), we will display nothing. If the answer is correct, we will display the correct result display, and if it is incorrect, we will display the incorrect result display:
displayResult(index) {
const question = this.state.questions[index];
if (!question.playerChoice) { return; }
if (question.playerChoice === question.rightAnswer) {
return (
<p className="result-correct">
Your answer is correct!
</p>
);
} else {
return (
<p className="result-incorrect">
Your answer is incorrect!
</p>
);
}
}
Finally, let's make it so we only display the question if the player has answered everything up to that point correctly.
- Modify our displayQuestion function to return nothing if the player's score is less than the index, preventing the display of further questions. This is pretty easy for us to write as a single conditional and return at the top:
if (this.state.playerScore < index) { return; }
If all has gone well, as you answer the questions, you should see each question being loaded in appropriately, as in the screenshot that follows:
Figure 3.9: Quiz app
And that's it; we now have a working quiz system. Now, let's pe right into how the looping technique works in React.
Rendering Loops with React
Another critical component of building dynamic apps with React is not just relying on conditional statements but also building upon standard loops in JavaScript to render multiple elements in simple ways. Think back to some of the work that we did as part of the last exercise: in our render call, we had this bit of code:
render() {
return (
<p className="App">
<h1>Quiz Show!</h1>
<hr/>
{this.displayQuestion(0)}
{this.displayQuestion(1)}
{this.displayQuestion(2)}
{this.displayQuestion(3)}
</p>
);
}
This is unnecessarily repetitive, and we can clean this up a lot with just a loop. Similar to how writing conditional rendering statements went, we can also write these either via inline statements in our JSX or through functions. For example, the preceding snippet could be written inline via the following:
render() {
return (
<p className="App">
<h1>Quiz Show!</h1>
<hr/>
{this.state.questions.map((question, index) => this.displayQuestion(index))}
</p>
);
}
Now we are getting a warning message that we should fix:
Note
Each child in a list should have a unique key prop.
We should absolutely fix that. Before that, though, let's pe a little deeper into rendering loops, because understanding what's going on will help us understand why we need to fix this in the first place.
Rendering Loops
We have used the phrase rendering loops a lot, but what does it actually mean? Essentially, we are telling React that rendering an element is the result of rendering a list of items, instead of just rendering a single item. So, let's say we have a tree structure like this:

Figure 3.10: Tree structure of a node
If we were to write this out in JSX, it would look something like this:
<node>
<child1 />
<child2 />
<child3 />
</node>
With the preceding JSX snippet as our example, the tree shown in Figure 3.10 is simple enough that React can figure out which element needs to change if child2 needs to be updated and not the rest of the tree. For example, it's easy for us to look at the tree example in Figure 3.10, point to child2, and say this needs to update. Now, if we were to build that list of children where the children nodes would change dynamically, it would be more abstract and difficult for React to interpret. Say, instead, our tree is as follows:
node
|-- children (length: 3)
Our JSX is as follows (this is just pseudocode):
<node>
{elements.map(child => <child />)}
</node>
Can you look at this and say update child 2? For that matter, how can React handle that? This scenario is where you would typically get the following error message in the console:
Warning: Each child in a list should have a unique "key" prop.
Let's instead say that each child has a special property on it called key, and that can tell React exactly where to look to update something. Now our graph is as follows:
node
|-- children (length: 3) [key: 0, key: 1, key: 2]
And our JSX pseudocode is as follows:
<node>
{elements.map((child, index) => <child key={index} />)}
</node>
Map is being used here specifically because we want to return the list of elements as we modify each of them; if we used a construct such as forEach, then we would not be returning a modified list.
Now, React can tell precisely where the second child is and it can update that without having to bother with the rest of the list of elements. Adding a key property to an element is just as simple as the preceding example: you provide a unique identifier called key, and it needs to be unique to all elements displayed on the page (not just in that particular subtree of rendered components).
Working with the same example, if we were to go into the displayQuestion() function, we could add a simple key property to the p at the top of the returned JSX with a unique identifier and it would remove that error message – something like this:
<p className="question-display" key={`q-${index}`}>
This would eliminate the error message completely.
Beyond that, everything else is just applying JavaScript techniques and rules to JSX syntax, something which, at this point, you should be very comfortable with.
Exercise 3.03: Refining Our Quiz Display with Loops
Using the app that we started to build in the previous exercise, we are going to clean up the code significantly using rendered loops to refactor and refine our exercise. This will be a little bit of a shorter exercise.
- Make sure the development server is running already, and if it is not, do so via yarn start:
$ yarn start
- To clean up our quiz display, we need to only render the questions from a list instead of us adding each question in manually. We will start off with that same example. Where we had multiple calls to this.displayQuestion(index), we will instead have a single-utility render function call:
render() {
return (
<p className="App">
<h1>Quiz Show!</h1>
<hr/>
{this.renderQuestions()}
</p>
);
}
We will also need to write the renderQuestions function, so let's take care of that while we're at it. In this function, we just need to iterate over each question and call displayQuestion for it, so we will use a map function call and pass in the index of the map call.
- Let's create the map function call:
renderQuestions() {
return this.state.questions.map((question, index) =>
this.displayQuestion(index)
);
}
- Reload the browser window (if it hasn't already happened) and verify that, so far, nothing has changed apart from an error message about there not being a unique key for each item rendered (which we can fix).
- Let's fix that error message by jumping to the top line of the return statement for displayQuestion() and adding a key property:
<p className="question-display" key={`q-${index}`}>
We still have some non-loop code that we can optimize, specifically in how we present the quiz choices to the user. In that same return statement, if you find the code that presents the choices, we can instead turn this into an inline call to map on question.possibleAnswers. Don't forget to give each answer choice its own unique key property.
- Place the following code inside the p you created previously:
{ question.possibleAnswers.map((answer, answerIndex) => (
<button key={`q-${index}-a-${answerIndex}`} className="question-choice" onClick={() => this.answerQuestion(index, answer)}>
{answer}
</button>
))}
Save the code and everything should still be functioning identically to before and should look identical as well. Our code is now much saner and cleaner, but the functionality remains intact.
The output is as follows:
Figure 3.11: Quiz show App
With all the knowledge gained from the chapter, it's time to put everything together that we have learned so far and build a fancy game board in the activity.
Activity 3.01: Building a Board Game Using Conditional Rendering
We are going to build a simple memory match game in React. We will create a list of tiles, randomize them, and assign simple number values to each. The idea is to match up different pairs. The rules of the game are simple:
- When a card is flipped, a number will be displayed, but otherwise will just be a plain backing.
- As we click on each card, we should "flip" the card, and only allow two cards to be flipped at a time.
- When a card is matched, it should be highlighted in green.
- When two cards do not match, the cards are flipped over again.
- Each attempt made by the player should be tracked; the player's score at the end is how many flips they made to match all the cards.
- The game is over when all matches have been made.
The CSS to use for building the app is provided here:
.Tile {
width: 200px;
height: 200px;
font-size: 64pt;
border: 2px solid #aaa;
text-align: center;
margin: 10px;
padding: 10px;
float: left;
cursor: pointer;
}
The following steps will help you achieve the goal:
- Set up the game board to display the cards. We will start with having it display 12.
- Create your React app with its constructor and define the tile count.
- Write the function that will generate the app when the New Game button is clicked. Several steps will be required to complete this. Steps 4-9 will help you to write the required function.
- Start off with a blank list of tiles.
- Generate a pair of numbered tiles per player, but only up to the max tile constant you created earlier.
- Each tile should have properties for whether the tile has been flipped over or not, whether the tile has been matched or not, and what number to display as the value of the tile.
- After building the tile, the tile should get added to the list of tiles.
- After building the list, the tiles should be randomized.
- Finally, the state of the application should be updated after this functionality has been built.
- Set up a click handler for each card that will flip the card when triggered.
- Set up some basic styling for each card to allow the player to see whether a card is already matched or not.
- Set up logic to check the current card flipped against a previous card if a card is already flipped over; if they match, add those to the matches.
- Keep track of every time the player attempts to make a match.
- Add a condition for when all the cards have been flipped over and the game is done.
There should be a New Game button to allow the player to set up and start a new game.
The output should look like this:

Figure 3.12: Memory Game app
And with that, we have built a memory game using conditional rendering in React.
Note
The solution of this activity can be found on page 615