💊
React状態管理ライブラリValtioで作る○×ゲーム
Reactの公式チュートリアルに三目並べゲーム (tic-tac-toe) の作り方があります。
それを元に(正確にはそのuseImmer版を元に)、Valtioで同じものを作ってみました。
ValtioはProxyベースのReact状態管理ライブラリで、仕組み的にはMobXに似ています。立ち位置的にはImmerに似ています。JavaScriptのオブジェクトをそのままReactの状態として使えるようにすることに特化しています。
READMEでは標準的な使い方としてはplain objectからproxyを作る方法を紹介していますが、今回はclassを使っています。TypeScriptで書きました。
コード
import "./styles.css";
import { proxy, useSnapshot } from "valtio";
import useWindowSize from "react-use/lib/useWindowSize";
import Confetti from "react-confetti";
type Player = "X" | "O" | null;
type Squares= [
Player, Player, Player,
Player, Player, Player,
Player, Player, Player,
] // prettier-ignore
class GameState {
lines = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]] as const // prettier-ignore
squares = new Array(9).fill(null) as Squares;
get nextValue() {
return this.squares.filter((r) => r === "O").length ===
this.squares.filter((r) => r === "X").length
? "X"
: "O";
}
get status() {
return this.winner
? `Winner: ${this.winner}`
: this.squares.every(Boolean)
? `Scratch`
: `Next player: ${this.nextValue}`;
}
get winner() {
for (let i = 0; i < this.lines.length; i++) {
const [a, b, c] = this.lines[i];
if (
this.squares[a] &&
this.squares[a] === this.squares[b] &&
this.squares[a] === this.squares[c]
)
return this.squares[a];
}
return null;
}
selectSquare(i: number) {
if (this.winner || this.squares[i]) return;
this.squares[i] = this.nextValue;
}
reset() {
this.squares = new Array(9).fill(null) as Squares;
}
}
const xoxo = proxy(new GameState());
function Square({ i }: { i: number }) {
const { squares } = useSnapshot(xoxo);
return (
<button
className={`square ${squares[i]}`}
onClick={() => xoxo.selectSquare(i)}
>
{squares[i]}
</button>
);
}
function Status() {
const { status } = useSnapshot(xoxo);
return (
<div className="status">
<div className="message">{status}</div>
<button onClick={() => xoxo.reset()}>Reset</button>
</div>
);
}
function End() {
const { width, height } = useWindowSize();
const { winner } = useSnapshot(xoxo);
return (
winner && (
<Confetti
width={width}
height={height}
colors={[winner === "X" ? "#d76050" : "#509ed7", "white"]}
/>
)
);
}
function App() {
return (
<>
<div className="game">
<h1>
x<span>o</span>x<span>o</span>
</h1>
<Status />
<div className="board">
{[0, 1, 2, 3, 4, 5, 6, 7, 8].map((field) => (
<Square key={field} i={field} />
))}
</div>
</div>
<End />
</>
);
}
export default App;
ちょっと長いでしょうか。ゲッターを使っているので多少マニアックです。無理して使わなくてもいいと思います。その場合は、通常の関数にしたりカスタムフックにしたりしますが、エレガントさは減るかも。
CodeSandbox
遊んでみてください。
関連記事
Discussion