LoginSignup
0
0

React入門5: リファクタリング [インタラクション編]

Last updated at Posted at 2023-12-06

はじめに

私自身の復習兼、備忘録的な意味もあり、複数回の記事に渡って React を用いてマルバツゲーム(三目並べ)を開発していきたいと思います。

シリーズの一覧

  1. React入門1: 環境構築 [オンライン版]
  2. React入門2: 盤面の作成
  3. React入門3: インタラクションの実装
  4. React入門4: リファクタリング [リフトアップ編]
  5. React入門5: リファクタリング [インタラクション編] (今回)
  6. React入門6: 手番の実装
  7. React入門7: ゲームの勝利判定
  8. React入門8: テキストの実装
  9. React入門9: タイムトラベル(1)
  10. React入門10: タイムトラベル(2)
  11. React入門11: タイムトラベル(3)

目的について

全体の目的

React公式のチュートリアルで公開されているマルバツゲームを 3x3 のマスで実装していきます。

今回の目的

前回のリフトアップで失われたインタラクションを実装して、リファクタリングを完了していきます。

リファクタリング(Part2)

次のページで、前回のソースファイルを確認できます。

  • 前回の内容はコチラから!

インタラクションの再実装

早速、コーディングをしていきます。App.js に次のように変更しましょう。

  • Square コンポーネントの変更
    • プロパティとして onSquareClick を追加する
    • <button> 要素に onClick プロパティを追加して、本コンポーネントの onSquareClick プロパティを渡す
  • Board コンポーネントの変更
    • handleClick() 関数を作成する
      • 仮引数はなし
      • コンソールにクリックされた旨を出力させる
    • 1つ目の Square コンポーネントに onSquareClick プロパティを追加して、 handleClick() 関数を渡す
App.js
import { useState } from "react";
import "./styles.css";

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() {
    console.log("clicked");
  }

  return (
    <>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={handleClick} />
        <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>
    </>
  );
}

コンソールを開いて( 表示方法: [画面の内容 > プレビュー > (A)] 参照 )、左上のマスに対してマウスクリックをすると、コンソール上に「clicked!」と表示されます。なお、左上以外のマスに対してクリックをしても、反応はありません。動作結果を次に示します(音がないのでクリックしていることが伝わりずらいと思いますが...)。

output_video.gif

左上にあるマスの表示変更

次に、マスがクリックされると「X」を表示するようにしていきます。App.js の Board コンポーネントで定義した handleClick() 関数を次のように変更しましょう。

  • nextSquares 配列を宣言する
    • 初期値として state 型の squares 配列のコピーを代入する
  • nextSquares 配列の第0要素に "X" を代入させる
  • state 型の squares 配列を nextSquares 配列に更新する
  • コンソールへの出力処理は削除する
handleClick() 関数の変更
function handleClick() {
  const nextSquares = squares.slice();
  nextSquares[0] = "X";
  setSquares(nextSquares);
}

これで左上のマスをクリックすると、「X」が表示されるようになりました。先ほどと同様にここでも、左上以外のマスに対してクリックをしても、反応はありません。

output_video.gif

全てのマスの表示変更

左上以外のマスでもクリックされたときに、「X」を表示できるように、App.js を次のように変更しましょう。

  • Board コンポーネントの変更
    • handleClick() 関数の変更
      • 仮引数 i を追加する
      • nextSquares 配列の第0要素に関する処理を削除する
      • nextSquares 配列の第 i 要素に "X" を代入させる
    • Square コンポーネントの onSquareClick プロパティを変更する
      • x 番目の Square コンポーネントにある onSquareClick プロパティには、handleClick(x) の処理を実行する関数を渡す( 解説 )
App.js
import { useState } from "react";
import "./styles.css";

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

実行結果を次に示します。どのマスもクリックされると、「X」が表示されるようになりました。

output_video.gif

再レンダーによる無限ループ

先ほどの リファクタリングの変更内容 で含みのある言い方をしましたね。

  • x 番目の Square コンポーネントにある onSquareClick プロパティには、handleClick(x) の処理を実行する関数を渡す

この内容は Board コンポーネントの return 文にある Square コンポーネントのプロパティに記述された「 onSquareClick={() => handleClick(0)} 」のような箇所が該当します。

これを「 onSquareClick={handleClick(0)} 」と書き換えてみましょう。

App.js
import { useState } from "react";
import "./styles.css";

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

この場合、onSquareClick プロパティに関数を渡すはずが、handleClick(0) という処理を実行することになります( 自動セミコロン挿入ASI によってセミコロン ; の省略は許容されます)。つまり、クリックのイベントと関数の発火が紐づきません。

イベントと関数が連携されていないのであれば、マスのクリックから処理が発生せず何も起きないただのバグになるはずです。つまり、アプリケーションの動作が停止するような致命的な問題とはなりえません。

それでは、次に実行結果を示します。

image.png

次のエラーが出力されてしまいます。

Error
Too many re-renders. React limits the number of renders to prevent an infinite loop.

これは、爆発的な回数の再レンダーから無限ループが発生している恐れがあり、React がアプリケーションの実行を停止させていることを示しています。

再レンダーの原因は handleClick() 関数の処理にある setSquares() 関数です。これは、前回の記事 で実装した state 型の squares 配列を更新する set 関数です。第3回の記事 に記したように、set 関数はコンポーネントを再レンダーする仕様となっています。

つまり、「 onSquareClick={handleClick(0)} 」と記述すると、0番目の Square コンポーネントを呼び出すときに handleClick(0) が実行され、setSquares() 関数の処理によって Board コンポーネントが再レンダーされます。そして、再レンダーされた Board コンポーネントが、0番目の Square コンポーネントを呼び出すときに handleClick(0) を実行して...、と処理が永遠に続きます。

再レンダーの無限ループ.jpg

そこで、Square コンポーネントのプロパティに無名関数を定義して渡すことで、再レンダーによる無限ループを解決していきます。正しい変更後のコード「 onSquareClick={() => handleClick(0)} 」は、アロー関数を用いて無名関数を定義しています。次のWebページの「解説」という項目を確認するとその成り立ちを確認できます。

『アロー関数式 - JavaScript | MDN』より引用
// 従来の無名関数
(function (a) {
 return a + 100;
});

// 1. "function" という語を削除し、引数と本体の開始中括弧の間に矢印を配置する
(a) => {
  return a + 100;
};

// 2. 本体の中括弧を削除と "return" という語を削除 — return は既に含まれています。
(a) => a + 100;

// 3. 引数の括弧を削除
a => a + 100;

定義した無名関数 ()onSquareClick プロパティに渡すことで、即時的に関数を実行せずに、イベントの発火(マウスクリック)に応じて処理されます。

おわりに

今回は、インタラクションを再実装することで、リフトアップによるリファクタリングを完了させました。次のページに現段階のソースファイルを示します。

次回は、マスが「X」だけでなく「O」も表示できるように実装していきます。

0
0
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
0
0