React状態管理ライブラリValtioで作る○×ゲーム

3 min read読了の目安(約3300字

https://twitter.com/dai_shi/status/1402258862753812481

Reactの公式チュートリアルに三目並べゲーム (tic-tac-toe) の作り方があります。

それを元に(正確にはそのuseImmer版を元に)、Valtioで同じものを作ってみました。

https://github.com/pmndrs/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

遊んでみてください。

関連記事

https://zenn.dev/dai_shi/articles/8258695a631c1a

https://zenn.dev/dai_shi/articles/f848fb75650753