Enable emmet for JSX in VSCode settings.
Start by deleting the contents of the <div className="App"></div>
element. Replace the contents with JSX for the select menu:
div>h2{Choose a region to study}+select>option[value=""]*6
Insert the following values, each corresponding with one option:
{""}
africa
americas
asia
europe
oceania
Insert the following content, each corresponding with one option:
---
Africa
Americas
Asia
Europe
Oceania
Change state with the select input by using the useState
hook.
Import useState
in line 1 of your App.js
.
import React, { useState } from "react";
Create a regionChoice
state variable by calling useState at the top of the definition of the App
functional component.
const [regionChoice, setRegionChoice] = useState(null);
Add an onChange
event handler to the select
input. Call it chooseRegion
. Control the select input with regionChoice
as the value
.
<select className="region-choice" onChange={chooseRegion} value={regionChoice}>
Under the useState call at the top of the App component definition, write the function definition for the chooseRegion
event handler. Open the browser console and test the onChange event with the select element.
function App() {
const [regionChoice, setRegionChoice] = useState(null);
function chooseRegion(e) {
console.log("chooseRegion: change!")
setRegionChoice(e.target.value);
setCountries([]);
}
Add another state variable to store fetched data.
function App() {
const [regionChoice, setRegionChoice] = useState(null);
const [countries, setCountries] = useState([]);
Go back to line 1 of App.js
to import useEffect.
import React, { useState, useEffect } from "react";
Before the component's return
statement, set up a useEffect
to be triggered by changes to the regionChoice
state. The useEffect's callback will fetch data when a region is chosen.
useEffect(
function() {
if (regionChoice !== null) {
fetch(`https://restcountries.eu/rest/v2/region/${regionChoice}`)
.then(response => response.json())
.then(newCountries => {
console.log(newCountries);
setCountries(newCountries);
});
}
},
[regionChoice]
);
Once the region data has been fetched, use conditional rendering to remove the select menu html from the DOM.
{
regionChoice === null && (
<div>
<h2>Select a region to study.</h2>
<select
className="region-choice"
value={regionChoice}
onChange={chooseRegion}
>
<option value={null}>---</option>
<option value="africa">Africa</option>
<option value="americas">Americas</option>
<option value="asia">Asia</option>
<option value="europe">Europe</option>
<option value="oceania">Oceania</option>
</select>
</div>
);
}
Although it hasn't yet been built, import the Card
component near the top of App.js
.
import React, { useState, useEffect } from "react";
import Card from "./Components/Card";
Once a region is chosen and the data is fetched, conditionally render the Card
component. Card
will accept
countries
as a prop.
</select>
</div>
)}
{regionChoice && countries && (
<Card
countries={countries}
></Card>
)}
</div>
);
}
Create a /src/Components/Card.js
component file and a /src/Components/Card.css
stylesheet.
Copy the following rule sets into the stylesheet.
.answer-wrapper {
min-height: 150px;
}
.question-mark {
padding-left: 40px;
font-size: 80px;
font-weight: bold;
}
.answer {
opacity: 0;
animation-name: answer-show;
animation-timing-function: linear;
animation-duration: 1s;
animation-fill-mode: forwards;
}
@keyframes answer-show {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.capital-name,
.country-name {
font-weight: bold;
}
.country-flag {
height: 100px;
}
.country-flag img {
height: 100%;
}
In Card.js
, import React and Card.css
. Write a function
declaration for the Card
component. Export the component.
import React from "react";
import "Card.css";
function Card() {
return <div className="card"></div>;
}
export default Card;
Create JSX for the Card
component.
.card>.prompt>p>span.capital-name^{ is the capital of}^.answer-wrapper>.answer>p.country-name+.country-flag>img
Card
destructures the props object to reference countries
.
console.log(countries)
to verify that data is being fetched and passed to
Card
via props.
function Card ({ countries }) {
console.log(countries);
Import useState
and create a state variable called country
. country
will hold the value of the country to be learned.
import React, { useState } from "react";
import "./Card.css";
function Card({ countries, returnToRegionMenu }) {
const [country, setCountry] = useState(null)
Import useEffect
and call it to pick the first country when the component mounts.
import React, { useState, useEffect } from "react";
...
...javascript
useEffect(() => {
const randomIndex = Math.floor(Math.random() * countries.length);
const newCountry = countries[randomIndex];
setCountry(newCountry);
}, [countries, country]);
If country
doesn't contain data, display an error message to the user.
if (!country) {
return <p><strong>Please reconnect to the internet and refresh the browser.</strong></p>;
}
return (
<div className="card">
Insert expressions into the JSX displaying the capital of the current country, the country name, and the country flag.
return (
<div className="card">
<div className="prompt">
<p>
<span className="capital-name">{country.capital}</span> is the capital
of
</p>
</div>
<div className="answer-wrapper">
<div className="answer">
<p className="country-name">{country.name}</p>
<div className="country-flag">
<img src={country.flag} alt={`flag of ${country.name}`}></img>
</div>
</div>
</div>
Conditionally render the answer with a new state variable, showAnswer
. Declare showAnswer
after country
, near the top of the component definition.
function Card({ countries, returnToRegionMenu }) {
const [country, setCountry] = useState(null);
const [showAnswer, setShowAnswer] = useState(false);
Setting the value to false
shows a question mark. Setting the value to
true
reveals the answer.
<div className="answer-wrapper">
{showAnswer === false && <div className="question-mark">?</div>}
{showAnswer === true && (
<div className="answer">
<p className="country-name">{country.name}</p>
<div className="country-flag">
<img src={country.flag} alt={`flag of ${country.name}`}></img>
</div>
</div>
)}
</div>
Set a timeout to delay the answer reveal by 5000 milliseconds. Only create a timeout when the answer gets hidden.
useEffect(() => {
if (showAnswer === false) {
setTimeout(() => {
setShowAnswer(true);
}, 5000);
}
}, [showAnswer]);
Create a Next Capital
button at the bottom of the component definition.
<img src={country.flag} alt={`flag of ${country.name}`}></img>
</div>
</div>
)}
</div>
<button onClick={handleNextClick}>Next Capital</button>
With the click event handler, give the user the ability to randomly pick a new country and reset the timer. Clear the value of the current country and hide the answer. The handler has logic to prevent country
from being reset multiple times while the answer is hidden.
}, 5000);
}
}, [showAnswer]);
function handleNextClick() {
if (showAnswer === false) {
return null;
}
setCountry(null);
setShowAnswer(false);
}
Create a Change Region
button at the bottom of the component definition.
<button onClick={handleNextClick}>Next Capital</button>
<button onClick={returnToRegionMenu}>Change Region</button>
</div>
);
}
The click handler will be defined in the parent component, App
, because setting the regionChoice
in App
to an empty string will render the select
menu instead of rendering the Card
component. For Card
click events to modify the state of App.js
, returnToRegionMenu
must be passed to Card
as a prop.
In Card.js
, reference returnToRegionMenu
by destructuring the props object.
function Card({ countries, returnToRegionMenu }) {
In App.js
, define returnToRegionMenu
and pass it to Card
as a prop.
function returnToRegionMenu() {
setRegionChoice("");
}
...
<Card
returnToRegionMenu={returnToRegionMenu}
countries={countries}
></Card>
When Card
is unmounted, the timeout is not cleared from memory. This leads to a memory leak whenever returnToRegionMenu
is called.
Declare a state variable, timerID
, to keep a reference to the timeout whenever setTimeout
is called. Its default value must be null until a timer starts.
function Card({ countries, returnToRegionMenu }) {
const [country, setCountry] = useState(null);
const [showAnswer, setShowAnswer] = useState(false);
const [timerID, setTimerID] = useState(null);
In the timer useEffect
, assign the timeout reference from setTimeout
to a variable.
Store the reference in the timerID
state in case returnToRegionMenu
is called.
useEffect(() => {
if (showAnswer === false) {
const timer = setTimeout(() => {
setShowAnswer(true);
setTimerID(null);
}, 5000);
setTimerID(timer);
}
When the Card
component unmounts, return a cleanup function from the useEffect to clear the timeout. timerID
must be added as a dependency of the useEffect.
}, 5000);
setTimerID(timer);
}
return function() {
clearTimeout(timerID);
};
}, [timerID, showAnswer]);
Finally, use the timerID
to make stricter conditions for when a new setTimeout
can be called.
if (showAnswer === false && timerID === null) {