React Tutorialを TypeScriptとHooksで書き直してみた⑤(fin)

タイムトラベル機能の追加|Stateのリフトアップ、再び

squaresを配列historyに保存しておき、あとで参照できるようにしていきます。

これまで、stateはBoardコンポーネントに実装していましたが、これをGameコンポーネントに移し、Boardにはpropsを渡すことによってアクセスできるようにします。

Gameコンポーネント

まず、GameコンポーネントにstateのhistoryxIsNextを設定します。

const [history, setHistory] = useState([{ squares: Array(9).fill(null) }]);
const [xIsNext, setXIsNext] = useState(true);

historyは括弧が多く少々分かりづらいですが、{squares: Array()}を要素に持つ配列です。

次に、handleClickメソッドをBoardコンポーネントからGameコンポーネントに移動します。
stateがsquaresからhistoryに変わったので、それに対応するように編集します。

const handleClick = (i: number): void => {
    const current = history[history.length - 1];
    const squaresSlice = current.squares.slice();

    // 勝者確定かマスが埋まっていたら、クリックしてもマスが変化しないようにする
    if (calculateWinner(squaresSlice) || squaresSlice[i]) {
      return;
    }

    squaresSlice[i] = xIsNext ? 'X' : 'O';
    setHistory(
      history.concat([
        {
          squares: squaresSlice,
        },
      ]),
    );
    setXIsNext(!xIsNext);
  };

まず、historyから最新のBoardの状態をcurrentで抜いて勝者判定をし、Boardが更新された後の状態squaresSlicehistoryに追加し、concatでhistoryにくっつけてstateにセットしています。

最後にGameコンポーネントの残りの部分です。

  const current = history[history.length - 1];
  const winner = calculateWinner(current.squares);
  const status = winner
    ? `Winner: ${winner}`
    : `Next player: ${xIsNext ? 'X' : 'O'}`;

  return (
    <div className="game">
      <div className="game-board">
        <Board squares={current.squares} onClick={(i) => handleClick(i)} />
      </div>
      <div className="game-info">
        <div>{status}</div>
        <ol>{/* TODO */}</ol>
      </div>
    </div>
  );

こちらも、handleClickと同様に、これまでBoardを直接参照していたところを、historyから最新の状態をcurrentに格納するようにしています。

また、状態をGameコンポーネントで管理するようになったので、Boardにはこれをpropsで渡せるようにします。

<Board squares={current.squares} onClick={(i) => handleClick(i)} />

squaresは今のBoardの状態、onClickはO/Xを配置する関数です。

ついでに、Boardに渡すpropsの型も定義しておきます。こちらはファイルの頭に追加します。

type BoardProps = {
  squares: FillSquare[];
  onClick: (i: number) => void;
};

Boardコンポーネント

まず、propsを受け取るようにしたいので、

const Board: VFC<BoardProps> = (props) => {
  const { squares, onClick } = props;

のように変更します。

さらに、公式チュートリアルと同じく、

Board の renderSquare にある this.state.squares[i] を this.props.squares[i] に置き換える。 Board の renderSquare にある this.handleClick(i) を this.props.onClick(i) に置き換える。

ことをします。

そして編集後は下のようになります。

const Board: VFC<BoardProps> = (props) => {
  const { squares, onClick } = props;

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

  return (
    <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>
  );
};

編集後のindex.tsx

githubこちら

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;
};

type BoardProps = {
  squares: FillSquare[];
  onClick: (i: number) => void;
};

const calculateWinner = (squares: FillSquare[]) => {
  const 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],
  ];
  for (let i = 0; i < lines.length; i += 1) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }

  return null;
};

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

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

const Board: VFC<BoardProps> = (props) => {
  const { squares, onClick } = props;

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

  return (
    <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 = () => {
  const [history, setHistory] = useState([{ squares: Array(9).fill(null) }]);
  const [xIsNext, setXIsNext] = useState(true);

  const handleClick = (i: number): void => {
    const current = history[history.length - 1];
    const squaresSlice = current.squares.slice();

    // 勝者確定かマスが埋まっていたら、クリックしてもマスが変化しないようにする
    if (calculateWinner(squaresSlice) || squaresSlice[i]) {
      return;
    }

    squaresSlice[i] = xIsNext ? 'X' : 'O';
    setHistory(
      history.concat([
        {
          squares: squaresSlice,
        },
      ]),
    );
    setXIsNext(!xIsNext);
  };

  const current = history[history.length - 1];
  const winner = calculateWinner(current.squares);
  const status = winner
    ? `Winner: ${winner}`
    : `Next player: ${xIsNext ? 'X' : 'O'}`;

  return (
    <div className="game">
      <div className="game-board">
        <Board squares={current.squares} onClick={(i) => handleClick(i)} />
      </div>
      <div className="game-info">
        <div>{status}</div>
        <ol>{/* TODO */}</ol>
      </div>
    </div>
  );
};

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

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

タイムトラベル機能の追加

過去の着手の表示

mapを用いて、過去の手番にジャンプするボタンを実装します。

// Gameコンポーネント内
  const moves = history.map((step, move) => {
    const desc = move ? `Go to move #${move}` : 'Go to game start';

    return (
      <li key={move.toString()}>
        <button type="button" onClick={() => jumpTo(move)}>
          {desc}
        </button>
      </li>
    );

keyを選ぶで述べられていますが、ここではkeyとしてmoveを選びます。keyはstring型なので、toString()で型を変換して渡しています。

タイムトラベルの実装

プレイ中のステップ数を記録するstateであるstepNumberを追加します。

const [stepNumber, setStepNumber] = useState(0);

次に、jumpTo関数を次のように実装します。

  const jumpTo = (step: number) => {
    setStepNumber(step);
    setXIsNext(step % 2 === 0);
  };

さらに、handleClickを編集し、タイムトラベルで手番を移動したときのhistoryと現在の手番であるstepNumberを更新するようにします。

  const handleClick = (i: number): void => {
    const historySlice = history.slice(0, stepNumber + 1);
    const current = historySlice[historySlice.length - 1];
    const squares = current.squares.slice();

    // 勝者確定かマスが埋まっていたら、クリックしてもマスが変化しないようにする
    if (calculateWinner(squares) || squares[i]) {
      return;
    }

    squares[i] = xIsNext ? 'X' : 'O';
    setHistory([...historySlice, { squares }]);
    setStepNumber(historySlice.length);
    setXIsNext(!xIsNext);
  };

公式チュートリアルとは異なり、historyで定義されている変数がhistorySliceになっていますが、これは名前の衝突が起こってしまうための回避策です。
historySliceは指定した手番までのhistoryを切り出し、currentは指定した手番のBoardの状態にしています。
さらに、

    setHistory([...historySlice, { squares }]);
    setStepNumber(historySlice.length);

を追加し、タイムトラベルした次の手番の状態を記録してstateを更新します。

あとは、return前の現在のBoardの状態と勝者判定の部分を次のように書き換えます。
これにより、stepNumberの手番を描画するようにします。

  const currentHistory = [...history];
  const current = currentHistory[stepNumber];
  const winner = calculateWinner(current.squares);

完成したコード

これで完成です!!

こちらから

やってみて

チュートリアルのリファクタは意外とすんなりいけました。useReducer(Redux)を使うとなお良いのかなと思います。

ブログを書くのが大変でした笑

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

手番の処理

ここからBoardコンポーネントを編集して手番ごとにXとOを入れ替えてプロットされるようにします。

編集後のBoardコンポーネントは以下のようになります。

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

  const handleClick = (i: number): void => {
    const squaresSlice = squares.slice();
    squaresSlice[i] = xIsNext ? 'X' : 'O';
    setSquares(squaresSlice);
    setXIsNext((c) => !c);
  };

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

  const status = `Next player: ${xIsNext ? 'X' : 'O'}`;

  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>
  );
};

詳しく見ていきます。

まず、stateにxIsNext: booleanを追加し、手番を保持します。以下の行に該当します。

  const [xIsNext, setXIsNext] = useState<boolean>(true);

次に、handleClick関数を編集し、手番ごとにXとOを入れ替えるようにします。

  const handleClick = (i: number): void => {
    const squaresSlice = squares.slice();
    squaresSlice[i] = xIsNext ? 'X' : 'O';
    setSquares(squaresSlice);
    setXIsNext((c) => !c);
  };

xIsNexttrueのときX、falseならOがプロットされます。

注意すべきは、setXIsNext((c) => !c);でしょうか。これはsetXIsNext(!xIsNext);でも正しく動作しますが、これは偶然です。

りあクト!2巻に記載がありますが、state変数はコンポーネントレンダリングごとで一定になるため、state変数を相対的に変更する処理を行うときはラムダ関数で書くべきとされています。

これは具体的にどういうことかというと、例えばxIsNextがXの状態で、setXIsNext(!xIsNext)を連続して2回行う処理

setXIsNext(!xIsNext);
setXIsNext(!xIsNext);

を実行すると、2回反転されるのでxIsNextはXになるはずですが、実際はOになります。これは1回目の処理が2回目の処理で上書きされるので、実質的に1回しか実行されないのと同じになります。

今回はこのような同じ処理を繰り返すことはないので、setXIsNext(!xIsNext)でも正常に動作するのですが、このような事情から、ラムダ式を使っています。

最後に、status変数を書き換えれば終了です。

  const status = `Next player: ${xIsNext ? 'X' : 'O'}`;

これで手番ごとにXとOが入れ替えるようになりました!!

ゲーム勝者の判定

まず、ファイルに勝者を判定する関数をコピペします。

const calculateWinner = (squares: FillSquare[]) => {
  const 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],
  ];
  for (let i = 0; i < lines.length; i += 1) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }

  return null;
};

公式チュートリアルで提供されているものに、引数の型を付しただけです。

次に、handleClick関数を編集し、マスがすでに埋まっているか、勝者が決まったらマスに入力できないようにします。

  const handleClick = (i: number): void => {
    const squaresSlice = squares.slice();

    // 勝者確定かマスが埋まっていたら、クリックしてもマスが変化しないようにする
    if (calculateWinner(squares) || squaresSlice[i]) {
      return;
    }

    squaresSlice[i] = xIsNext ? 'X' : 'O';
    setSquares(squaresSlice);
    setXIsNext((c) => !c);
  };

最後に、statusを次のように書き換え、勝者判定されたら勝者を表示するように変更します。

  const winner = calculateWinner(squares);
  const status = winner
    ? `Winner: ${winner}`
    : `Next player: ${xIsNext ? 'X' : 'O'}`;

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

これでゲームは完成です。次はタイムトラベル機能を作っていきます!

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;のように受け取って使っているシンプルなコードです。


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

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

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

前回からの続きです。

データを Props 経由で渡す

マスに数字がプロットされた次のようなアウトプットを想定します。

https://ja.reactjs.org/static/685df774da6da48f451356f33f4be8b2/01bf6/tictac-numbers.png

公式チュートリアルでは、

class Board extends React.Component {
  renderSquare(i) {
    return <Square value={i} />; // 追加
  }
}

// ︙

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {this.props.value} // "TODO"になっていたところを変更
      </button>
    );
  }
}

でOKでした。

TypeScriptを使う

まず、Squareコンポーネントに渡されるpropsの型エイリアスSquarePropsを定義します。

type SquareProps = {
  value: number;
};

Squareコンポーネントに渡されるのはvalueで、これは0~8の数字なので、number型にします。

次に、Squareコンポーネントは下のようになります。

const Square: VFC<SquareProps> = ({ value }) => (
  <button type="button" className="square">
    {value}
  </button>
);

ジェネリクスVFC<SquareProps>のように書くことで、コンポーネントのpropsの型を指定できます。
SquarePropsからvalueを抜き出し、buttonに表示するシンプルなコードです。

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

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

ここは公式チュートリアルと同じです。

  <button
    type="button"
    className="square"
    onClick={() => {
      alert('click');
    }}
  >

チュートリアルにも書かれていますが、注意するポイントはonClick={() => { alert('click'); }です。よくある間違いとして記載してあります。

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

次回はStateの取り扱いから書いていこうと思います!

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

概要

前回記事で書いた「りあクト!」を読んで学んだことですが、現在のReactでは関数コンポーネントが主流になっており、公式チュートリアルでメインに扱われているクラスコンポーネントはあまり用いられないようです。 また、実践的な開発では、JavaScriptよりもTypeScriptが主に使われています。

そこで、ReactチュートリアルをTypeScript、関数コンポーネント、Hooksで書き直してみました。 流れは公式チュートリアルのままになっています。

※とはいえ、stateを理解するにはクラスコンポーネントを理解しておくと良さそうなので、いきなりHooksを使うより、一度公式チュートリアルをやってよかったかなと思います。

私がトライしたときに作ったコードはこちらです↓

github.com

主な環境

  • macOS 11.4
  • node 14.4.0
  • TypeScript 4.3.2
  • React 17.0.2

チュートリアルの準備

基本的にはこの公式チュートリアルの流れに従って準備します。

手順

  1. npx create-react-app my-app --template=typescript でプロジェクトを作成
  2. 作成されたmy-app/src/配下のファイルを全部消す
  3. src配下に、index.tsxを作成し、このコードをコピペ
  4. 同じくindex.cssを作成し、このコードをコピペ

スターターコードの中身

このindex.tsxは、公式チュートリアルのスターターコードindex.jsを関数コンポーネントとTSで書き直したものです。

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

const Square: VFC = () => (
  <button type="button" className="square">
    {/* TODO */}
  </button>
);

const Board: VFC = () => {
  const renderSquare: VFC = () => <Square />;

  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'));

各関数コンポーネントにくっついてるVFCは、関数コンポーネントの型を表します。

具体的にはpropsを引数にとり、ReactElementもしくはnullを返す関数として定義されているオブジェクトです。

VoidFunctionComponentエイリアスになっていて、VFCと表現できます。

参考
(Reactの関数コンポーネントの型には、元々FunctionComponent型があったようですが、現在推奨されていないようです。「りあクト!」にも同様の説明がありました。こちらの記事がわかりやすかったです)

個人的に気になったこと

JS/TSには関数閉包(クロージャ)という概念が存在し、関数の中に関数を定義することができます。 (正確には、関数の中に関数を定義できるから、関数閉包ができると言ったほうがいいかもしれません)

C++Pythonを主に扱ってきた私としては、なかなか奇妙な感じがします。
このコードでも、次の箇所に使っています。

const Board: VFC = () => {
  const renderSquare: VFC = () => <Square />;

関数閉包については、こちらのページがわかりやすかったです。

余計な説明が多くなりましたが、次回からこのコードをチュートリアルに従って三目並べを作っていきます!

「りあクト!」を読んでみた!

f:id:riku929hr:20210613234011p:plain Reactの勉強によいと評判の「りあクト!」シリーズを購入してみました。

booth.pm

booth.pm

booth.pm

昨年改定され、全3巻になったらしい。ボリュームはかなりあります。 2巻の基礎編まで読み終わりました。ここまで読んだ感想を簡単に。

とにかくわかりやすい!

一言でいうとこれにつきます。いいなと思ったポイントをまとめると、

  1. 対話形式で進行しており、読みやすい
  2. 最悪JavaScriptを知らなくても読み進められ、JSで躓きやすいポイントがコンパクトにまとまっている
  3. TypeScriptの事前知識不要
  4. Reactの特徴や、今の仕様(関数コンポーネント、Hooks)に至った経緯が詳しい
  5. Webの情報では収集しきれないようなベストプラクティスが多数掲載

1. 対話形式

Ruby on Railsを使っていたが、急にフロントエンドに回ることになり、React+TSを短期間で覚えないといけなくなった、というシチュエーションで書かれています。文中ではReactのMVCフレームワークとの比較や、JS/TSとRubyJavaの多言語との対比を用いて解説されている部分が多いです。

2. JavaScriptを知らなくても読める

1巻がJS、TSの言語・環境編になっています。変数の宣言、関数、クラス、無名関数など、Reactを書くのに必要な知識は、JSをほとんど知らない人でも習得できるようになっているように感じました。

特に秀逸だったのはthisの挙動の理解です。thisを暗黙のうちに与えられた引数として考えるという考え方は非常にわかりやすかったです。

また、関数型プログラミングの解説が厚いのも、知らなかった私としてはありがたかったです。

ただし、本編ではJSを全く知らない人は「JavaScript Primer」という入門書を先に読むように指南されています。

3. TypeScriptの事前知識不要

型がつくことで何がありがたいのかから解説されています。要点が絞られておりわかりやすいです。

4. Reactの特徴や仕様変遷が詳しい

VueやAngularと何が違うのかなど、理解に役立つネタが数多く載っています。 なぜクラスコンポーネントではなく関数コンポーネントになったのか、どういう経緯でHooksができたのか、かなり詳しく載っています。

5. ベストプラクティスが多数掲載

npmのインストールから丁寧に記載されています。とくにLinter等の環境設定が詳しく掲載されていたのはありがたいです。

読んでみて

全3巻、600ページほどあるため、読むのに体力が必要なところが弱点ですが、公式のドキュメントを読むより早く知識が整理されると思います。

私はReactの公式チュートリアル(Tic Tac Toe)を一通りやった後に読みました。 公式チュートリアルはクラスコンポーネント+JSで書かれているのですが、これを関数コンポーネント+TSでリファクタしてみました。

github.com

コードは不完全だと思いますが、一通りできるようになったのは良い収穫です。 (詳しい方いらっしゃればコードの誤りなどご指摘いただけると喜びます)

りあクト!はおすすめ

同人誌の技術書ってどうなん?って思ってましたが、とても良かったのでレビュー記事書きました。 Reactの学習に悩んでいる方、おすすめです。