2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

関数コンポーネントで学ぶReact公式チュートリアル

Last updated at Posted at 2021-06-20

関数コンポーネント版React公式チュートリアル

Reactの公式チュートリアルではクラスコンポーネントが使われていますが、最近は関数コンポーネントを利用することがほとんどだと思います。折角初心者の方に教えるのであれば、使えるものを教えたいと思ったので、公式チュートリアルを一通り関数コンポーネントを使った形に書き換えてみました。
以下にはコードと関数コンポーネントで書く際の注意点のみ記述します。
コードの詳しい解説については、公式の方を確認してください。

スターターコード

サンプルコードに関してはCodeSandboxに用意しました。以降で操作するファイルは、src/index.jsになります。
CodeSandbox

描画したいJSXを、クラスコンポーネントではrenderメソッドに記述しましたが、関数コンポーネントでは返り値とするのでreturn文のなかに記述します。

また、以下のコードでは関数の定義は、基本的にアロー関数を変数に代入する形でおこないます。

const コンポーネント名=(props)=>{
  /* 処理 */
  return <Component value={props.value}/>
}

この場合、定義順などに気をつける必要がありますが、同名コンポーネントの定義を防止できるなど利点もあるので、この形を利用していきます。
JavaScriptの関数定義に興味のある方は、@raccy様の記事を参考にしてみてください。

ゲームを完成させる

データをProps経由で渡す

BoardrenderSqueareを以下のように書きえましょう。

const renderSquare = (i) => <Square value={i} />;

こうすることでSquareの中で、引数iの値をprops.valueとして扱うことができます。実際にSquareを書き換えてprops.valueの値を反映させてみましょう。

const Square = (props) => {
  return <button className="square">{props.value}</button>;
};

各マスに数字が表示されるようになったと思います。

インタラクティブなコンポーネントを作る

クラスコンポーネントではコンストラクタのthis.stateでまとめてステートを管理しますが、関数コンポーネントではHook(今回は'useState'関数)と言われるものを利用して一つ一つステートを定義します。

const [state, setState]=useState(初期値);

変数名(state)は何でもいいですが、setStateの部分は必ずset+変数名(キャメルケース)にしてください)。
ステートの値が使いたい場合は、普通の変数と同じようにstateを使いましょう。
しかし、ステートの値を変更する際は直接代入するのではなく、各ステートに対応した関数を使う点には注意してください。上記のコードを用いると以下のようになります。

//Bad
state=1;
//これだとDOMが更新されない
//そもそもconstで定義しているので代入はできませんが・・・

//Good
setState(1);
//ちゃんとDOMが更新されます

Squareを書き換えると以下のようになります。

const Square = (props) => {
  const [value, setValue] = useState("");
  return (
    <button className="square" onClick={() => setValue("X")}>
      {value}
    </button>
  );
};

State のリフトアップ

これについては、ステートの定義方法が違うだけでクラスコンポーネントと同じ手法を用います。以下に変更結果を示します。
完成品

const Square = (props) => {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
};

const Board = (props) => {
  const [squares, setSquares] = useState(Array(9).fill(null));

  const handleClick = (i) => {
    const newSquares = squares.slice();
    newSquares[i] = "X";
    setSquares(newSquares);
  };

  const renderSquare = (i) => (
    <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 Board = (props) => {
  const [squares, setSquares] = useState(Array(9).fill(null));
  const [xIsNext, setXIsNext] = useState(true);

  const handleClick = (i) => {
    const newSquares = squares.slice();
    newSquares[i] = xIsNext ? "X" : "O";
    setSquares(newSquares);
    setXIsNext(!xIsNext);
  };

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

  const status = `Next player: ${xIsNext ? "X" : "O"}`;
//以下変更なし

ゲーム勝者の判定

Board内で関数を定義する、引き分けを追加するなど変更点はありますが大体同じです。

const Board = (props) => {
  const [squares, setSquares] = useState(Array(9).fill(null));
  const [xIsNext, setXIsNext] = useState(true);

  const handleClick = (i) => {
    const newSquares = squares.slice();
    if (calculateWinner(newSquares) || newSquares[i]) {
      return;
    }
    newSquares[i] = xIsNext ? "X" : "O";
    setSquares(newSquares);
    setXIsNext(!xIsNext);
  };

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

  const calculateWinner = (squares) => {
    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++) {
      const [a, b, c] = lines[i];
      if (
        squares[a] &&
        squares[a] === squares[b] &&
        squares[a] === squares[c]
      ) {
        return squares[a];
      }
    }
    return null;
  };

  const status = () => {
    const winner = calculateWinner(squares);
    if (winner) {
      return "Winner: " + winner;
    } else if (squares.reduce((x, y) => x && y)) {
      return "Draw";
    } else {
      return `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 のリフトアップ、再び

historyステートはオブジェクトのリストではなく、2次元リストに変更しました。
その他定義順や記法が一部異なります。

const Board = (props) => {
  const renderSquare = (i) => (
    <Square
      value={props.squares[i]}
      onClick={() => {
        props.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 = (props) => {
  const initSquares = Array(9).fill(null);
  const [history, setHistory] = useState([initSquares]);
  const [xIsNext, setXIsNext] = useState(true);

  const calculateWinner = (squares) => {
    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++) {
      const [a, b, c] = lines[i];
      if (
        squares[a] &&
        squares[a] === squares[b] &&
        squares[a] === squares[c]
      ) {
        return squares[a];
      }
    }
    return null;
  };

  const status = (squares) => {
    const winner = calculateWinner(squares);
    if (winner) {
      return "Winner: " + winner;
    } else if (squares.reduce((x, y) => x && y)) {
      return "Draw";
    } else {
      return `Next player: ${xIsNext ? "X" : "O"}`;
    }
  };

  const current = history.slice(-1)[0];
  const winner = calculateWinner(current);

  const handleClick = (i) => {
    const squares = current.slice();
    if (winner || squares[i]) return;
    squares[i] = xIsNext ? "X" : "O";
    setHistory([...history, squares]);
    setXIsNext(!xIsNext);
  };

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

過去の着手の表示&Key を選ぶ

関数の定義順や場所に注意したこと以外に変更はありません。
これがチュートリアルの最後であるため、全文載せておきます。

import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./index.css";

const Square = (props) => {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
};

const Board = (props) => {
  const renderSquare = (i) => (
    <Square
      value={props.squares[i]}
      onClick={() => {
        props.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 = (props) => {
  const initSquares = Array(9).fill(null);
  const [history, setHistory] = useState([initSquares]);
  const [xIsNext, setXIsNext] = useState(true);
  const [stepNumber, setStepNumber] = useState(0);

  const calculateWinner = (squares) => {
    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++) {
      const [a, b, c] = lines[i];
      if (
        squares[a] &&
        squares[a] === squares[b] &&
        squares[a] === squares[c]
      ) {
        return squares[a];
      }
    }
    return null;
  };

  const status = (squares) => {
    const winner = calculateWinner(squares);
    if (winner) {
      return "Winner: " + winner;
    } else if (squares.reduce((x, y) => x && y)) {
      return "Draw";
    } else {
      return `Next player: ${xIsNext ? "X" : "O"}`;
    }
  };

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

  const handleClick = (i) => {
    const slicedHistory = history.slice(0, stepNumber + 1);
    const squares = current.slice();
    if (winner || squares[i]) return;
    squares[i] = xIsNext ? "X" : "O";
    setHistory([...slicedHistory, squares]);
    setXIsNext(!xIsNext);
    setStepNumber(slicedHistory.length);
  };

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

  const moves = history.map((step, move) => {
    const desc = move ? "Go to move #" + move : "Go to game start";
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{desc}</button>
      </li>
    );
  });

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

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

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

まとめ

以上が関数コンポーネント化させたReact公式チュートリアルになります。関数内で関数を定義したり、クロージャーを用いていたりと見慣れない書き方かもあるかもしれませんが、Reactや関数型プログラミングではよく見る光景なので慣れておくといいと思います。
一部筆者の好みで改変したため、こうした方が公式に近い、より良い書き方があるという方は、コメントなどでアドバイスをいただけると幸いです。
Reactは非常に便利なライブラリですので、これを活用してどんどんアプリケーションを作っていきましょう!

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?