簡単なゲームを作ってみたくなったので、連打ゲーを作ってみました!
ユーザーの操作は
- 連打する秒数を設定する
- clickボタンを連打する
のみです!
クリックされるたびにカウントアップするだけだと面白くないので
- 設定された秒数の間は、何回クリックしたのはユーザーは確認できない
- 設定された秒数が経過すると、クリックした回数がわかる
- ただ回数を出すのは面白くないので、数字が増えていく様子が見えるようにする
という仕様で作りたいと思います!
- ただ回数を出すのは面白くないので、数字が増えていく様子が見えるようにする
完成物
キャプチャ
コード
import { ChangeEvent, useState } from "react";
function App() {
const button = document.querySelector("button");
const [time, setTime] = useState(1);
const handleChangeTime = (e: ChangeEvent<HTMLInputElement>) => {
setTime(Number(e.target.value));
};
const [count, setCount] = useState(0);
const handleClick = () => {
if (time <= 0) {
alert("秒数は1以上を設定してください");
return;
}
setTimeout(() => {
setCount((prev) => prev + 1);
if (button) {
button.disabled = true;
}
}, time * 1000);
};
const resetGame = () => {
setCount(0);
if (button) {
button.disabled = false;
}
};
return (
<div>
<input type="number" value={time} onChange={handleChangeTime} />
<span>秒間クリックしまくれ!!</span>
<h1>{count}</h1>
<button onClick={handleClick}>click!</button>
<button className="reset" onClick={resetGame}>
reset
</button>
</div>
);
}
export default App;
N秒後にstateを更新したい
- 設定された秒数の間は、何回クリックしたのはユーザーは確認できない
- 設定された秒数が経過すると、クリックした回数がわかる
- ただ回数を出すのは面白くないので、数字が増えていく様子が見えるようにする
この仕様を実現するために「ユーザーがクリックしているときにcountを更新を保留し、N秒経過した後に、保留していたcountの更新を行う」という実装を試みました。
失敗例
const [count, setCount] = useState(0);
const handleClick = () => {
// 一部処理を省略しています
setTimeout(() => {
setCount(count + 1);
}, time * 1000);
};
この例ではsetCount
にcount + 1
を渡しています。これだとうまくいきません。
ユーザーがボタンをクリックしてから(handleClick
が発火してから)time秒後にsetCount()
が実行され、現在のcount
に1が足されるので問題ないように見えますが、この実装だとcount
が1以上になることはありません。
これは古いクロージャによる更新を行なってしまっていることが原因です。
setTimeout()
に設定した秒数の間にボタンをクリックした時、setTimeout()
に渡されているコールバック関数は同じクロージャになります。つまり、1度目のクリックも2度目のクリックも、コールバック関数が() => setCount(0 + 1)
と解釈されるということです。
なので、count
が1になった後に更新されないのです。
わかりやすい解説
https://speakerdeck.com/recruitengineers/react-yan-xiu-2024?slide=125
成功例
これはsetCount()
に(prev) => prev + 1
というコールバックを渡すことで解決します。
const [count, setCount] = useState(0);
const handleClick = () => {
// 一部処理を省略しています
setTimeout(() => {
setCount((prev) => prev + 1);
}, time * 1000);
};
prev
には(prev) => prev + 1
が実行されるときのcount
の値が入ります。
なので、クリックした回数分、count
が更新されます。
妥協点
レンダリング効率
count
が更新されるたびにApp.tsx全体が再レンダリングされるので、本当はcount
に関する処理のみを担当するコンポーネントを作成して、count
更新による影響範囲を最小限にするべきだと思います。
面倒くさかったのでやりませんでした。
結果発表方法
結果発表時に一定間隔で数値を増やしていきたかったのですが、実装が大変そうだったので妥協しました。
今はクリックしてから4秒後にcountを更新する、という実装になっているため、結果発表時にクリック回数がスムーズに増えていかないことがあります。例えば、一度クリックした後に2秒待ってから再度クリックする、という動作を行うと、結果発表時にもcountが1→2になるのに2秒かかります(タスクキューに入るのに時間差がある)。
とはいえ、ゲームの趣旨的に制限時間いっぱいまでユーザーはボタンを連打し続けてくれだろうと考え、改修は先送りにしました。