はじめに
ReactとTypeScriptについて一通り学んだので、コードに慣れることを目的として何かアプリを作りたいと思いました。前回の記事で超基本的な実装をして、useState, useEffectなど基本的な機能がぼんやり理解できてきたので、調子に乗ってさらに機能を追加してみようという趣旨です。
やること
スコア(正解数)を記録する
コードはこちら
import { useState, useEffect } from 'react';
import './App.css';
interface QuizData {
question: string;
correct_answer: string;
}
function App() {
const [question, setQuestion] = useState<QuizData | null>(null);
const [loading, setLoading] = useState<boolean>(true);
// 何問目かを管理するStateを追加
const [count, setCount] = useState<number>(1);
// ①正解数を管理するステート(初期値は0)
const [score, setScore] = useState<number>(0);
const fetchQuiz = async () => {
setLoading(true);
try {
const res = await fetch("https://opentdb.com/api.php?amount=1&type=boolean");
const data = await res.json();
setQuestion(data.results[0]);
} catch (error) {
console.error("エラー:", error);
} finally {
setLoading(false);
}
};
// "などが含まれるのでこれを " にデコードする関数
const decodeHTML = (html: string) => {
const txt = document.createElement("textarea");
txt.innerHTML = html;
return txt.value; // " が " になった純粋な文字列が手に入る
};
// countが変わるたびに新しいクイズを取得
useEffect(() => {
fetchQuiz();
}, [count]);
const handleAnswer = async (userAnswer: string) => {
if (!question) return;
if (userAnswer === question.correct_answer) {
// ②正解したときだけ、スコアを1増やす
setScore((prev) => prev + 1);
alert("正解!🎉 次の問題に進みます。");
// カウントを増やす(これがきっかけで useEffect が再実行される)
setCount((prev) => prev + 1);
} else {
alert("残念!😢 次の問題に進みます。");
// ③カウントを増やす(これがきっかけで useEffect が再実行される)
setCount((prev) => prev + 1);
}
};
if (loading) {
return (
<div className="container">
<h1>第 {count} 問</h1>
{/* ★追加:現在のスコアを表示 */}
<p style={{ fontWeight: 'bold', color: '#00b894' }}>
現在の正解数: {score}
</p>
<p className="question-text">読み込み中...</p>
</div>
);
}
if (!question) return null;
return (
<div className="container">
{/* 何問目かを表示 */}
<h1>第 {count} 問</h1>
<p>
{/* ④このpタグの中でscoreを表示 */}
現在の正解数: {score}
</p>
{/* decodeHTMLをここで使う */}
<p className="question-text">{decodeHTML(question.question)}</p>
<div className="button-group">
<button className="btn-true" onClick={() => handleAnswer("True")}>True</button>
<button className="btn-false" onClick={() => handleAnswer("False")}>False</button>
</div>
</div>
);
}
export default App;
1. const [score, setScore] = useState<number>(0);
// ①正解数を管理するステート(初期値は0)
const [score, setScore] = useState<number>(0);
useStateでscoreと、setScoreを設定。TypeScriptなのでデータ型をnumberに設定。初期値は0になります。
2.setScore((prev) => prev + 1);
// ②正解したときだけ、スコアを1増やす
setScore((prev) => prev + 1);
setScoreの中身が1増えているので、scoreの値が1増加して、画面が再レンダリングされます。ちなみにReactのState更新では、直接 score + 1 と書くと、稀に古い値を参照して計算がズレることがあります。(prev) => prev + 1 と書くことで、「その処理が実行される瞬間の最新の値」 を確実に使って計算できるため、より安全です。
3.setCount((prev) => prev + 1);
// ③カウントを増やす(これがきっかけで useEffect が再実行される)
setCount((prev) => prev + 1);
前回までは不正解の場合は同じ問題に引き続き答える形式でしたが、今回は間違えても次の問題に進むことにしました。ここでsetCount()の中身が1増えてcountが変更されるので、、、
useEffect(() => {
fetchQuiz();
}, [count]);
このコードが発火。この中でfetchQuiz()関数が発火して、新たなクイズを取得→useState()で定義されたsetQuestion()が実行され、新たなクイズの内容が画面に表示されます。このあたりは慣れないと追いついていくのが大変です。
4. scoreの値を表示
<p>
{/* ④このpタグの中でscoreを表示 */}
現在の正解数: {score}
</p>
setScore((prev) => prev + 1);が更新されてscoreの値が1増加して、画面が再レンダリングされることで、画面上では「現在の正解数」の値が1増加することになります。
という形で、正解数のカウントをリアルタイムで表示できるようになりました。
問題発覚
しかし、ここでクイズアプリとして問題が発覚。問題に回答した後、2、3回同じ問題がたて続けに出題される現象が発生するのです。
APIのドキュメントを見てみると下記の表示を見つけました。
The API appends a "Response Code" to each API Call to help tell developers what the API is doing.
(途中省略)
Code 5: Rate Limit Too many requests have occurred. Each IP can only access the API once every 5 seconds.
つまりAPIは5秒以内にたて続けにリクエストを送るとエラーを返すという仕組みになっていたのです。
クイズが出題されてから速攻で回答すれば、1秒もかかりません。そうすると次の問題を取ってこようとしてもエラーが返ってくるため、同じ問題が出題されてしまうというわけです。
次回APIのこの仕様に対応してクイズの形式を微調整しようと思います。
