Help us understand the problem. What is going on with this article?

Reactチュートリアルメモ

REACTのチュートリアルについてのメモです

1. 開発環境を作る

公式チュートリアルに沿って進めていきます
https://ja.reactjs.org/tutorial/tutorial.html

ブラウザ上で開発するかローカルで開発するか

ブラウザ上で開発出来るスターターコードを使えば開発環境の整備は不要ですが、今回僕はローカルでやりました。

ローカル環境を作る

以下の手順でローカルのプロジェクトを作成してサーバーを起動します

1. Nodejsをインストールする

公式サイトからダウンロードしてインストールします
https://nodejs.org/ja/

2. Create React Appでプロジェクトフォルダを作る

適当なディレクトリで以下を実行します

terminal
npx create-react-app my-app
3. サーバーを起動する

上記を実行するとmy-appというディレクトリが作成されて、デフォルトで必要なファイルが生成されていますので、my-appに移動してnpm startすればサーバーが待機します。

terminal
cd my-app
npm start

http://localhost:3000/ を開くと以下のように表示される筈です

チュートリアルでは作成されたファイルを使用しないので/src/の中のファイルを全部消して作り直します

LinuxまたはMacの場合
cd my-app
cd src
rm -f *
cd ..
Windowsの場合
cd my-app
cd src
del *
cd ..

2. Gameクラスを作成して表示する

チュートリアルでは以下のような3目並べのゲームボードを作成します
https://codepen.io/gaearon/pen/gWWZgR?editors=0010
image.png
これを実現するため、1マスに該当するSquareクラスを9個並べてBoardクラスにして、これをGameクラスのプロパティとしてもたせた上で、ReactDOM.render()に表示してもらいます。

/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {/* TODO */}
      </button>
    );
  }
}

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

  render() {
    const status = 'Next player: X';

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

class Game extends React.Component {
  render() {
    return (
      <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')
);
/src/index.css
body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

これで先程と同様にnpm startして http://localhost:3000/ にアクセスすると、以下のように表示される筈です。
image.png

3. クラスに値を持たせる/値を参照する

以下のようにしてやればSquareクラスに文字を表示できます

index.js
class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {"a"}
      </button>
    );
  }
}

classに持たせた値を参照して表示する場合には以下のようにします。Square.render()内でthis.props.valueを参照しているので、Squareクラスのprops(プロパティ)のvalueを参照していることになります。

index.js
class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {this.props.value}
      </button>
    );
  }
}

BoardクラスからSquareクラスを作る際に引数としてvalueプロパティを作るようにします。

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

Boardクラスの引数通りに0~8の番号を表示するようになりました
image.png

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

buttonにonClick={}を追加することでSquareクラスのオブジェクトがクリックされたときにalertを出すようにしてみましょう。以下のように関数を渡してやることでクリックされたときにreactがalert()を出してくれます。

index.js
class Square extends React.Component {
  render() {
    return (
      <button className="square" onClick={function() { alert('click'); }}>
        {this.props.value}
      </button>
    );
  }
}

なお、上記のようにfunctionが重なるとthisがどれを指しているのか分かりにくくなるので下のようにarrow関数で書く方が良いそうです。

index.js
class Square extends React.Component {
 render() {
   return (
     <button className="square" onClick={() => alert('click')}>
       {this.props.value}
     </button>
   );
 }
}

5. クリックしたらXを表示するようにする

まず、コンストラクタ(クラスが最初に呼ばれた時に実行される関数)を追加して状態を記憶させるthis.stateを初期化するようにします。最初にsuper(props)としてるのは、ES2015(ES6)のJavaScriptクラスではsuper()クラスを呼ぶまでthisもsuperも使えなくなる仕様になっているからだそうで、コンストラクタを書くときは常に最初にsuper(props)するように推奨されています。

続いて、buttonに表示される値をthis.state.valueに変更し、onClickでthis.state.valueを'X'に変更するようにします。

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

  render() {
    return (
      <button className="square" onClick={() => this.setState({value: 'X'})}>
        {this.state.value}
      </button>
    );
  }
}

クリックするとコマにXが表示されるようになりました
image.png

6. React DevTools

ChromeまたはFirefoxの拡張機能として提供されているReact DevToolsを使うとReactアプリケーションのpropsとstateを確認できるようになります。

インストール手順は以下を参照してください
https://ja.reactjs.org/docs/optimizing-performance.html

拡張機能を有効にすると、ブラウザの開発者向けツールにreactのcomponentsタグとProfilerタグが追加されて、各クラスのプロパティが確認できるようになります。
image.png

7. 子コンポーネントの値を親コンポーネントに監理させる

ここまでのコードではSquareクラスが自分で表示するための値を保持していましたが、親クラスに監理させた方がコードが分かりやすく、より壊れにくく、リファクタリングしやすくなるそうですので、そのように書き換えていきます。

まず、親クラスにコンストラクタ関数を書いてBoard.state.squaresを初期化、この値を使ってSquareオブジェクトを表示してやります。また、先程までSquareクラスに書いてあったonClickで呼び出す関数もBoardクラスにhandleClick()として書いてやります。これにより値の監理をやりやすくなります。

また、handleClick()内でthis.state.squaresの値を一旦slice()してから書き換えているのはコードをイミュータブルにする為です。元の値を直接いじらないイミュータブルな書き方にする事で変更の有無を検出しやすくしたり、変更の履歴を保存したり、render()すべきタイミングを把握しやすくする効果が期待できるそうです。

index.js
class Board extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      squares: Array(9).fill(null),
    };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares: squares});
  }

  renderSquare(i) {
    return (
      <Square 
        value={this.state.squares[i]} 
        onClick={() => this.handleClick(i)} 
      />
    );
  }

Squareでやっていた値の監理がなくなったのでコンストラクタは消し、onClickでは親クラスから受け取ったSquare.props.onClick()関数(中身はBoard.handleClick())をonClickで実行するように書いてやります。

index.js
class Square extends React.Component {
  render() {
    return (
      <button 
        className="square" 
        onClick={() => this.props.onClick()}
      >
        {this.props.value}
      </button>
    );
  }
}

クリックするとBoardクラスにstateの値が変わり、それに合わせてXが表示されていくようになりました。
image.png

これによりSquaredはクリックされたことを親クラスに伝えるだけのコンポーネントになって、ロジックが書きやすくなりました。

8. 関数コンポーネント

上記の変更によりSquareクラスに値を持たなくなったのでクラスでなく関数で書いたほうが簡潔になります。{() => this.props.onClick()}の代わりにprops.onClickと書き換えるので、以下のようにかなり短く書くことができます。

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

9. 手番の処理

Xの手番の次はOにならないといけないので、次の手番がXOのどちらなのか書いてやります。BoardクラスにxIsNextプロパティを設定して、trueだったら次はXの手番、falseだったら次はOの手番として処理します。

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

class Board extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,
    };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

  renderSquare(i) {
    return (
      <Square 
        value={this.state.squares[i]} 
        onClick={() => this.handleClick(i)} 
      />
    );
  }

  render() {
    const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

image.png

10. 勝者を判定する

勝者を判定する関数を作成します。squaresは初期値がnullなのでif文からするとfalseと扱われるのを利用してnullではない値が何れかのlineすべてに入っていたら勝者は入っていた値であるという関数ですね。

index.js
function 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;
}

Board.render()内でcalculateWinner()を使って勝者判定を行い、勝者を表示するようにします。

render() {
  const winner = calculateWinner(this.state.squares);
  let status;
  if (winner){
    status = 'Winner: ' + winner;
  } else {
    status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
  }

また、勝者が決まった後に手番が進められるとおかしいので、Board.handleClick()内でcalculateWinner()がnullでないときは手番の更新を行わないようにします。

handleClick(i) {
  const squares = this.state.squares.slice();
  if (calculateWinner(squares) || squares[i]) {
    return;
  }
  squares[i] = this.state.xIsNext ? 'X' : 'O';
  this.setState({
    squares: squares,
    xIsNext: !this.state.xIsNext,
  });
}

image.png

11. 履歴の保存

履歴を残しておいて手番を戻ることができるようにします。具体的には、以下のようなフォーマットで履歴を保存しておいて、戻れるようにしてやります。

こんな風に保存したい
history = [
  // Before first move
  {
    squares: [
      null, null, null,
      null, null, null,
      null, null, null,
    ]
  },
  // After first move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, null,
    ]
  },
  // After second move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, 'O',
    ]
  },
  // ...
]

上記のhistoryをGame.props.statesとして管理するように書き換えます。

handleClick()でhistoryに最新のsquaresを追加して上書きすることで1つずつ追加される挙動を作っています。

チュートリアルではこれに並行してBoardクラスで管理してた値をまとめてGameクラスで管理するように変更する作業もやってますので、以下のようにごっそり書き換えてしまってますが、自分でやるときはまず値の管理を置き換えて、正常に動いてから機能追加した方が良いと思います。

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

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

class Board extends React.Component {
  renderSquare(i) {
    return (
      <Square 
        value={this.props.squares[i]} 
        onClick={() => this.props.onClick(i)} 
      />
    );
  }

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

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      xIsNext: true,
    }
  }

  handleClick(i) {
    const history = this.state.history;
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares
      }]),
      xIsNext: !this.state.xIsNext,
    });
  }

  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

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

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

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

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

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

これで、Gameクラス内に履歴が残るようになりました。
image.png

12. 履歴の表示

とりあえず表示

ここでJavaScriptの標準機能である配列のmapメソッドを使います

mapメソッドの動作例
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]

Game.render()内でhistoryの値からbutton表示させるhtmlを生成し、olタグ内に貼り付けることで戻るためのbuttonを作ります。

index.js
  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

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

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

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

この時点ではbuttonタグのonClick()で呼ばれるjumpTo()を書いていないのでbuttonを押すとエラー停止になります。
image.png

historyにkeyを追加

この時点で実行するとconsoleに以下のような警告がでます
image.png
リスト項目に固有のkeyが置かれていないので良くないですよ、と警告されています。順番だけで管理してると項目が増えた時に混乱するのでチュートリアルにも「動的なリストを構築する場合は正しい key を割り当てることが強く推奨されます」とありますから、素直に従ってkeyを追加してやります。

具体的には

{hoge(move)}を{hoge(move)}と書き換えます。reactがkey={move}を認識してそこに書かれたhoge(move)にGame.state.moveを渡せば良いんだと理解してくれます。
  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

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

警告が出なくなってくれました
image.png

jumpTo()を書く

戻るための関数を書いていきます。まずconstructorに現在表示している手番が何番目かを示すstepNumberを設定し、jumpTo()するとstepNumberとxIsNextをその時点での値に戻し、その後render()する際に表示する盤面を最新のものhistory[history.length - 1]ではなく指定されたものhistory[stepNumber]に変更、handleClick()されたら手戻りしたところまでのhistoryに追加して書き加えていくようにします。

jumpTo()の時点でhistoryを書き換えてしまうと履歴をウロウロすることが出来なくなっちゃいますので新たに手番が指されたときに更新するようにしてるわけですね。

index.js
class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      stepNumber: 0,
      xIsNext: true,
    }
  }

  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares
      }]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext,
    });
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0,
    });
  }

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

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

ということで完成したコードがこちらです
https://codepen.io/gaearon/pen/gWWZgR?editors=0010

13. buildする

アプリケーションが完成しましたのでデプロイ用にbuildしましょう。

まず、相対パスで書けるようにpackage.jsonに"homepage": "./"を追記します。

package.json
{
  "name": "my-app",
  "version": "0.1.0",
  "homepage": "./",

あとはプロジェクトフォルダでnpm run buildするだけです

terminal
cd my-app
npm run build

プロジェクトフォルダ内にbuildフォルダが生成されています
image.png
このindex.htmlをブラウザで開くとアプリケーションが表示されます
image.png

14. 次に何をやれば良いか

この先、ドキュメントはreact.jsの主なコンセプトの解説へと続いていきます。JSXやstateのライフサイクル、イベント処理やReactの流儀など重要な項目にフォーカスして説明しているので続いて読んでいくのが良さそうです。
https://ja.reactjs.org/docs/hello-world.html

  1. Hello World
  2. JSX の導入
  3. 要素のレンダー
  4. コンポーネントと props
  5. state とライフサイクル
  6. イベント処理
  7. 条件付きレンダー
  8. リストと key
  9. フォーム
  10. state のリフトアップ
  11. コンポジション vs 継承
  12. React の流儀

感想

チュートリアルをやる前にREACTは学習コストが高くて云々という記事も多くみかけたんですが、classベースで書くやり方も情報を上位クラスで保持する書き方もそこそこ複雑なコードを書く時の作業性に効いてきそうですし、デバッグがやりやすいようにブラウザ拡張ツールでエラーコードが確認できるのもありがたいです。少なくとも、生のJavaScriptよりは大幅にコードが書きやすいのでreact.jsを食わず嫌いしてるウェブ開発ビギナーはチュートリアルだけでもやってみてほしいです。

studio_haneya
製造業でデータサイエンティスト的な仕事をやってます
https://twitter.com/studio_haneya
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away