React 公式チュートリアル 解説 (第3回)

React

前回のおさらい

マス目をクリックし、consoleに表示されるところまで行いました。

ボタンを押してマスを変更する

次は、Squareコンポーネントがクリックされたら「X」に表示が切り替わるよう実装します。

ここで出てくるReactの重要概念が「state」です。

stateはコンポーネントの状態を保持することができます。

早速使ってみましょう。

import { useState } from "react"; //追記

function Square({ value }) {
  const [value, setValue] = useState(null);//追記
  function handleClick() {
    console.log("clicked!");
  }
  return (
    <button className="square" onClick={handleClick}>
      {value}
    </button>
  );
}

まず1行目で「useState」をimportします。

次にuseStateを使ってstateを定義します。

const [ 状態を保持する値、状態を更新する関数 ] = useState(初期値)

という風に記述します。

useStateは、左辺で状態を保持するvalueと、それを更新する関数setValueを受け取ります。

propsは一旦使わないので削除してしまいます。

import { useState } from "react";

function Square() {
  const [value, setValue] = useState(null);
  function handleClick() {
    console.log("clicked!");
  }
  return (
    <button className="square" onClick={handleClick}>
      {value}
    </button>
  );
}

export default function Board() {
  return (
    <>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
    </>
  );
}

☝️props削除後の状態

クリックしたら「X」が出るように変更します。

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

console.logの箇所をsetValue(“X”)に変更します。

これで画面でマスをクリックすると、押されたマスが「X」に変わります!!

ここで大事なのは、押されたマスのみが「X」に変わる、ということです。

他のマスも押してみましょう。

押したマスと関係ない部分は変わりません。

すなわち、次のようなことがいえます。

  • stateはコンポーネントの中で閉じていて、他のマスに影響を与えない
  • コンポーネント1つ1つが独立している

ここまでの全体のコードは次のようになります。

import { useState } from "react";

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

export default function Board() {
  return (
    <>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
    </>
  );
}

マス目の状態管理

ここまでで、マス1つ1つの状態を変更することができました。

でもこれでは、勝敗の判定ができません。

三目並べゲームの勝敗を確認するためには、9つのSquareコンポーネントの状態をボード側が把握する必要があります。

つまり、マス目のstateをボード側で管理するよう変更する、ということです。

この「状態」を誰(どのコンポーネント)が管理するか?がReact開発では非常に重要になります。

早速試してみましょう。Board コンポーネントを編集して、squares という名前の状態変数を宣言し、デフォルトで 9 つの正方形に対応する 9 つの null の配列になるようにします。

import { useState } from "react";

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

export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));//追記
  return (
    <>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
    </>
  );
}

squaresには

[‘O’, null, ‘X’, ‘X’, ‘X’, ‘O’, ‘O’, null, null]

のように、9つのマスの状態が配列で保持されるイメージです。

初期値はArray(9).fill(null)

でnullが9つ入った配列です。

Board側で状態を保つため、Square側にその値を渡さなければなりません。

親のコンポーネントから、子のコンポーネントに値を渡すのは、、、「props」でした。

一旦削除したpropsを復活させます。

import { useState } from "react";

function Square({value}) {
// propsを復活、useStateはBoardで管理するので削除、handleClickも削除
  return (
    <button className="square">// onClickも削除
      {value}
    </button>
  );
}

export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));
  return (
    <>
      <div className="board-row">
        <Square value={squares[0]} />//propsに追記
        <Square value={squares[1]} />
        <Square value={squares[2]} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} />
        <Square value={squares[4]} />
        <Square value={squares[5]} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} />
        <Square value={squares[7]} />
        <Square value={squares[8]} />
      </div>
    </>
  );
}

これでボードからマスに値を渡せるようになりました。

空のボードになったはずです。

次に、SquareがクリックされたときにBoard コンポーネントが持っているマス目の情報を更新する必要があります。

stateはBoardコンポーネントの

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

で作成しました。

更新するための関数は「setSquares」です。

これを使って、Squareがクリックされたときにマスを変更します。

まず、Squareコンポーネントを修正します。

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

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

onClickを復活させ、「onSquareClick」という名前の関数を入れます。onSquareClickはpropsにも追加します。

これでBoard側から、Square側にクリックされた時の挙動を決めることができるようになります。

次にBoard側です。

export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));
  function handleClick() {
    const nextSquares = squares.slice();
    nextSquares[0] = "X";
    setSquares(nextSquares);
  }
  return (
    <>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={handleClick} />
        <Square value={squares[1]} onSquareClick={handleClick} />
        <Square value={squares[2]} onSquareClick={handleClick} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onSquareClick={handleClick} />
        <Square value={squares[4]} onSquareClick={handleClick} />
        <Square value={squares[5]} onSquareClick={handleClick} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onSquareClick={handleClick} />
        <Square value={squares[7]} onSquareClick={handleClick} />
        <Square value={squares[8]} onSquareClick={handleClick} />
      </div>
    </>
  );
}

各Squareに、先ほど定義したonSquareClickの関数を渡します。

渡す関数は「handleClick」とし、useStateの下の行で定義します。

handleClickの中身を解説します。

const nextSquares = squares.slice();

Array.sliceは配列をコピーするメソッドです👇

Array.prototype.slice() - JavaScript | MDN
slice() は Array インスタンスのメソッドで、配列の一部を start から end (end は含まれない)までの範囲で、選択した新しい配列オブジェクトにシャローコピーして返します。 ここで start と end はその配列に含まれる項目のインデックスを表します。元の配列は変更されません。

nextSquaresにはsquaresのコピーが入ります。

次にnextSquares[0] = “X”;

で配列の最初の値を「X」に変更しています。

変更後の値をsetSquaresに入れ、ボードの値を更新しています。

(ここでなぜコピーしているかは、後に説明します。)

試しにマスをクリックします。

左上のマスが変わります。

配列の0番目を指定しているので、左上しか変わりません。

では、次のように関数に引数をとり、マス目のindexを受け取るようにします。

これでどのマスでも更新できるようになるはず、、、

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

handleClickに引数を受け取り、配列のi番目を変更するようにします。

      <div className="board-row">
        <Square value={squares[0]} onSquareClick={handleClick(0)} />//変更
        <Square value={squares[1]} onSquareClick={handleClick} />
        <Square value={squares[2]} onSquareClick={handleClick} />
      </div>

上のように先頭のマスのhandleClickに0を入れてみます。

するとどうでしょう。

画面が真っ白になってしまいました!!

これは、(0)をつけたことで関数が呼び出されてしまったために起こります。

JavaScriptでは、関数の後ろに()をつけるとその関数が実行されるからです。

関数の定義と呼び出しが不安な人はこちらをみると良いでしょう。

関数をpropsで渡すときや、onClickなどのイベントハンドラに関数を渡すときは

関数を実行してはいけない」ということを覚えておきましょう。

これを解決するには次のようにします。

<Square value={squares[0]} onSquareClick={()=>handleClick(0)} />

「 ()=> 」はアロー関数です。

アロー関数で包むことで、関数の実行をしないで済んでいます。

(この辺はややこしいと思うので、今はへえ〜と思うくらいで大丈夫です。)

これを全てのマスに適用しましょう。

import { useState } from "react";


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


export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));
  function handleClick(i) {
    const nextSquares = squares.slice();
    nextSquares[i] = "X";
    setSquares(nextSquares);
  }
  return (
    <>
     <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
      </div>
    </>
  );
}

無事に動きましたでしょうか?

まとめ

  • マス目の状態管理を、各々のSquareコンポーネントからBoard コンポーネントに移動した。
  • Boardコンポーネントで状態を管理することで、今後ゲームの勝敗判定がしやすくなる。
  • 親が持つステートを子コンポーネントから更新するには、propsで更新用関数を受け渡す。

続きは次回にします。