🗒️

Reactのチュートリアルが新しくなったので4年ぶりにやってみた

2024/05/21に公開

はじめに

reactのチュートリアルが新しくなったようなので↓、Viteを使ってやってみることにしました。
https://ja.react.dev/learn/tutorial-tic-tac-toe

↓が自分が昔やった、チュートリアルです。確かにuseStateが使われて無いですね。
https://github.com/na8esin/react-study/blob/master/src/index.js
しかもGitHub Pagesで公開されていますね。すっかり記憶の彼方ですが。

セットアップ

まずは、npm create vite@latestを実行します。構成は↓です。

✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC

もちろんreactのチュートリアルは、typescriptじゃ無いので、ところどころcopilotさんの力を借ります。
それと、もともとviteのテンプレートとして存在したcssは削除して、三目並べのcssを持ってきます。
※チュートリアルに埋め込まれてるcodesandboxをForkなどして、別のブラウザで開いてコピペ

実際にやってみる

↓あたりまでは、内容が簡単なので一気に飛ばします。
https://ja.react.dev/learn/tutorial-tic-tac-toe#completing-the-game

そして、ここで、Square コンポーネントを下記のように編集するんですが、暗黙のAny型になってるのでエラーが出てます。

↓あたり読めばなんとなくわかる気がしますが、
https://ja.vitejs.dev/guide/features.html#typescript
npm run devで実行中は、implicitly has an 'any' typeが出てても実行できます。

ですが、buildするとちゃんとエラーが出てくれます。

$ npm run build

> vite-practice@0.0.0 build
> tsc && vite build

src/App.tsx:3:18 - error TS7031: Binding element 'value' implicitly has an 'any' type.

3 function Square({value}) {
                   ~~~~~

自分は、typescriptの書き方はより厳密に行きたいタイプなので、エラーを修正したいと思います。
とは言え、自分は普段react書かないので、関数の引数で分割代入が行われるときの型指定とか、覚えてません。
こういう時は、copilotさんにお願いします↓
https://youtu.be/RyqIpeq4tq8
webフロント系は、結構役に立ってくれる印象があります。

まあでも、落ち着いて、ドキュメントをよく見ればこの辺りに書いてありました。
https://ja.react.dev/learn/typescript

こうなるとPropsのinterfaceも定義したくなります。

interface SquareProps {
  value: string | null;
  onSquareClick: () => void;
}

function Square({value, onSquareClick}: SquareProps) {
  return <button className="square" onClick={onSquareClick}>{value}</button>;
}

こんな感じになるかと思います。
ちょっと話がそれますが、AWS CDKのPropsを定義するときに似てますね(同じ?)。
Terraformを使うことの方が多いですが、なんでも使ってみるもんだなと思いました。

そしてそうこうすると
https://ja.react.dev/learn/tutorial-tic-tac-toe#declaring-a-winner
まで進んで、とりあえずゲームが出来上がります。

ここでこんな気になるコードがありました。

  let status;
  if (winner) {
    status = 'Winner: ' + winner;
  } else {
    status = 'Next player: ' + (xIsNext ? 'X' : 'O');
  }

  return (
    <>
      <div className="status">{status}</div>
  // 以下略

letの部分が気になるんですが、2024年現在、まだif式みたいなものは導入されてないようです。
IIEFというのを使って、こんな書き方もあるようです。
https://typescriptbook.jp/reference/functions/iife#ifやswitchなどを式として扱いたい場合
こういう書き方は嫌いじゃないんですが、可読性が上がってるか微妙だったので、これくらいなら三項演算子でいいかなと思いました。ついでにコンポーネント化してみました↓
https://github.com/na8esin/react-vite-practice/blob/fdcb572b0aab64ae367d53d8d8592bced3c81abc/src/App.tsx#L38-L69

あとはタイムトラベル機能を作れば完成です。
https://ja.react.dev/learn/tutorial-tic-tac-toe#adding-time-travel

Boardに3つのpropsを受け入れられるようにするので、ここもinterfaceを作らないと読みづらそうです。

interface BoardProps {
  xIsNext: boolean;
  squares: (string | null)[];
  onPlay: (squares: (string | null)[]) => void;
}

function Board({ xIsNext, squares, onPlay }: BoardProps) {
  // ...
}

それと、1手目から最終手までの全ての盤面を記録するhistoryというstateも型が明示されてないと若干わかりづらかったので、こんな風にしました。

type History = (string | null)[][];

export default function Game() {
  const [history, setHistory] = useState<History>([Array(9).fill(null)]);
  // ...
}

追加実装

これだけだと物足りないので、追加の機能を実装して行きます。
https://ja.react.dev/learn/tutorial-tic-tac-toe#wrapping-up

現在の着手の部分だけ、ボタンではなく “You are at move #…” というメッセージを表示するようにする。

の部分ですが、こんな感じかと思います。

export default function Game() {
  // ...

  const moves = history.map((_, move) => {
    const descriptionButton = (move > 0)?
      'Go to move #' + move:
      'Go to game start';

    const description = (move === 0)?
      'You are at Game start':
      'You are at move #' + move;

    return (
      <li key={move}>
        {move === currentMove ?
          (<b>{description}</b>) :
          (<button onClick={() => jumpTo(move)}>{descriptionButton}</button>)
        }
      </li>
    );
  })
}

マス目を全部ハードコードするのではなく、Board を 2 つのループを使ってレンダーするよう書き直す

これは最初こんな感じで、やってみました。

  const boardRows =
    [0, 1, 2].map((i) => {
      return (
        <div key={i} className="board-row">
          {[0, 1, 2].map((_, j) => (
            <Square
              key={j + 3*i}
              value={squares[j + 3*i]}
              onSquareClick={() => handleClick(j + 3*i)} />
          ))}
        </div>
      );
    });

  return (
    <>
      <CurrentGameStatus winner={winner} xIsNext={xIsNext} />
      {boardRows}
    </>
  );
}

でも、これでいい気がしてきました。

index.css
.board-container {
  width: fit-content;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}
App.tsx
return (
    <>
      <CurrentGameStatus winner={winner} xIsNext={xIsNext} />
      <div className="board-container">
        {
          Array.from({ length: 9 }, (_, i) =>
            <Square
              key={i}
              value={squares[i]}
              onSquareClick={() => handleClick(i)} />)
        }
      </div>
    </>
  );

ここまでのソース

https://github.com/na8esin/react-vite-practice/tree/829e0175ff4fb52a741253b55650fb4d30f58615

記事の分量がいい感じになりましたので、一旦ここで区切りたいと思います。

しくみのテックブログ

Discussion