rikuto tech blog

ゆる〜くやってます

React Tutorialを TypeScriptとHooksで書き直してみた③

前回の続きです。 いよいよ三目並べづくりに入ります。

インタラクティブなコードをつくる

盤面のマスをクリックするとXが現れるようにします。

はじめにSquareコンポーネントstateを持たせます。

公式チュートリアルではこのように実装されています。

class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

  render() {
    return (
      <button className="square" onClick={() => alert('click')}>
        {this.props.value}
      </button>
    );
  }
}

useStateを使って関数コンポーネントで実装

関数コンポーネントではHooksの関数であるuseStateを用いて実装できます。 以下が実装したコードです。

const Square: VFC<SquareProps> = () => {
  const [value, setValue] = useState<string | null>(null);
  // useState(' ')でもOKだが、チュートリアルに合わせてnullが代入できるように型を設定する

  return (
    <button
      type="button"
      className="square"
      onClick={() => {
        setValue('X');
      }}
    >
      {value}
    </button>
  );
};

const [value, setValue] = useState<string | null>(null);ですが、useStateはこの配列の第一引数にstate、第2引数にstateのセッターとなる関数が代入されます。ここではvaluesetValueとしています。

useStateジェネリクスによりstateの型を設定できます。ここではstringまたはnull型としています。

また、useStateの引数にはstateの初期値を代入でき、ここではチュートリアルに従いnullに設定しています。

表示部分の修正

return内のonClickを少し変更します。

  • onClick={() => this.setState({value: 'X'})onClick={() => {setValue('X');}}に変更
  • valueの値を表示する部分で、{this.state.value}{value}に変更

これによりJS特有のthisの挙動に悩む必要がなくなるため、大きなメリットになっています。

割愛しますが、JSのthisの挙動は4種類あり、混乱を招く要因となっているようです(りあクト!より)。

ここまでのコードはこちら

ゲームを完成させる

ようやく三目並べの完成パートです。

Stateのリフトアップ

前のステップで、stateを各Squareコンポーネントに持たせていましたが、Boardコンポーネントにstateを持たせて、Squareコンポーネントからアクセス、変更するようにします。

完成したコードがこちら↓です。

import { VFC, useState, ReactElement } from 'react';
import ReactDOM from 'react-dom';
import './index.css';

// Squareの中身の型、XかOか空(null)の3通り
type FillSquare = 'X' | 'O' | null;

type SquareProps = {
  value: FillSquare;
  onClick: () => void;
};

const Square: VFC<SquareProps> = (props) => {
  const { value, onClick } = props;

  return (
    <button type="button" className="square" onClick={onClick}>
      {value}
    </button>
  );
};

const Board: VFC = () => {
  const [squares, setSquares] = useState<FillSquare[]>(Array(9).fill(null));

  const handleClick = (i: number): void => {
    const squaresSlice = squares.slice();
    squaresSlice[i] = 'X';
    setSquares(squaresSlice);
  };

  const renderSquare = (i: number): ReactElement => (
    <Square value={squares[i]} onClick={() => handleClick(i)} />
  );

  const status = 'Next player: X';

  return (
    <div>
      <div className="status">{status}</div>
      <div className="board-row">
        {renderSquare(0)}
        {renderSquare(1)}
        {renderSquare(2)}
      </div>
      <div className="board-row">
        {renderSquare(3)}
        {renderSquare(4)}
        {renderSquare(5)}
      </div>
      <div className="board-row">
        {renderSquare(6)}
        {renderSquare(7)}
        {renderSquare(8)}
      </div>
    </div>
  );
};

const Game: VFC = () => (
  <div className="game">
    <div className="game-board">
      <Board />
    </div>
    <div className="game-info">
      <div>{/* status */}</div>
      <ol>{/* TODO */}</ol>
    </div>
  </div>
);

// ========================================

ReactDOM.render(<Game />, document.getElementById('root'));

Boardコンポーネント

const [squares, setSquares] = useState<FillSquare[]>(Array(9).fill(null));

ここは前のステップと同じで、squaresというstateを設定しています。 このstateの型はFillSquareの配列になっていて、初期値にはnullで満たされた要素数9の配列が渡されます。

FillSquareという型エイリアスはプログラムの先頭付近で設定されていて、type FillSquare = 'X' | 'O' | null;となっています。これは、各SquareコンポーネントにはXOnullしか代入されないからです。

renderSquare

次に、renderSquare関数ですが、TypeScriptでは次のように書けます。 const renderSquare = (i: number): ReactElement => ( <Square value={squares[i]} onClick={() => handleClick(i)} /> );

引数と戻り値の型が設定され、number型のiを引数にとり、ReactElementを返す関数であることがひと目で分かるようになりました。 C++を最もよく使ってきた私としては、型が明示されるとすごくスッキリします。

なお、クラスコンポーネントで書かれていたthisも消えています。

handleClick

公式チュートリアルと順番が前後しますが、ここでhandleClick関数についても書いておきます。

クリックされたマスをXにする関数です。stateのsquaresslice()でコピーし、i番目のマスをXにしたあと、新しい配列をstateにセットし直しています。

stateを直接書き換えないのは、非破壊的であることが望ましいとされる関数型プログラミングの思想によるものです(まだ関数型プログラミングを完全に理解しきれていませんが…)。 公式チュートリアルでは、「イミュータビリティはなぜ重要なのか」のパートで解説されています。

Squareコンポーネント

最後にSquareコンポーネントです。

type SquareProps = {
  value: FillSquare;
  onClick: () => void;
};

const Square: VFC<SquareProps> = (props) => {
  const { value, onClick } = props;

  return (
    <button type="button" className="square" onClick={onClick}>
      {value}
    </button>
  );
};

はじめに、propsの型を定義しています。FillSquare型のvalueと、戻り値を持たない関数onClickを持ちます。

これをコンポーネント定義内でconst { value, onClick } = props;のように受け取って使っているシンプルなコードです。


これでチュートリアルもようやく半分くらいでしょうか。

残りも頑張って書いていきます!