はじめに
私自身の復習兼、備忘録的な意味もあり、複数回の記事に渡って React を用いてマルバツゲーム(三目並べ)を開発していきたいと思います。
シリーズの一覧
- React入門1: 環境構築 [オンライン版]
- React入門2: 盤面の作成
- React入門3: インタラクションの実装
- React入門4: リファクタリング [リフトアップ編]
- React入門5: リファクタリング [インタラクション編] (今回)
- React入門6: 手番の実装
- React入門7: ゲームの勝利判定
- React入門8: テキストの実装
- React入門9: タイムトラベル(1)
- React入門10: タイムトラベル(2)
- React入門11: タイムトラベル(3)
目的について
全体の目的
React公式のチュートリアルで公開されているマルバツゲームを 3x3 のマスで実装していきます。
今回の目的
前回のリフトアップで失われたインタラクションを実装して、リファクタリングを完了していきます。
リファクタリング(Part2)
次のページで、前回のソースファイルを確認できます。
- 前回の内容はコチラから!
インタラクションの再実装
早速、コーディングをしていきます。App.js に次のように変更しましょう。
-
Square
コンポーネントの変更- プロパティとして
onSquareClick
を追加する -
<button>
要素にonClick
プロパティを追加して、本コンポーネントのonSquareClick
プロパティを渡す
- プロパティとして
-
Board
コンポーネントの変更-
handleClick()
関数を作成する- 仮引数はなし
- コンソールにクリックされた旨を出力させる
- 1つ目の
Square
コンポーネントにonSquareClick
プロパティを追加して、handleClick()
関数を渡す
-
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!」と表示されます。なお、左上以外のマスに対してクリックをしても、反応はありません。動作結果を次に示します(音がないのでクリックしていることが伝わりずらいと思いますが...)。
左上にあるマスの表示変更
次に、マスがクリックされると「X」を表示するようにしていきます。App.js の Board
コンポーネントで定義した handleClick()
関数を次のように変更しましょう。
-
nextSquares
配列を宣言する- 初期値として
state
型のsquares
配列のコピーを代入する
- 初期値として
-
nextSquares
配列の第0要素に"X"
を代入させる -
state
型のsquares
配列をnextSquares
配列に更新する - コンソールへの出力処理は削除する
function handleClick() {
const nextSquares = squares.slice();
nextSquares[0] = "X";
setSquares(nextSquares);
}
これで左上のマスをクリックすると、「X」が表示されるようになりました。先ほどと同様にここでも、左上以外のマスに対してクリックをしても、反応はありません。
全てのマスの表示変更
左上以外のマスでもクリックされたときに、「X」を表示できるように、App.js を次のように変更しましょう。
-
Board
コンポーネントの変更-
handleClick()
関数の変更- 仮引数
i
を追加する -
nextSquares
配列の第0要素に関する処理を削除する -
nextSquares
配列の第i
要素に"X"
を代入させる
- 仮引数
-
Square
コンポーネントのonSquareClick
プロパティを変更する-
x
番目のSquare
コンポーネントにあるonSquareClick
プロパティには、handleClick(x)
の処理を実行する関数を渡す( 解説 )
-
-
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」が表示されるようになりました。
再レンダーによる無限ループ
先ほどの リファクタリングの変更内容 で含みのある言い方をしましたね。
x
番目のSquare
コンポーネントにあるonSquareClick
プロパティには、handleClick(x)
の処理を実行する関数を渡す
この内容は Board
コンポーネントの return
文にある Square
コンポーネントのプロパティに記述された「 onSquareClick={() => handleClick(0)}
」のような箇所が該当します。
これを「 onSquareClick={handleClick(0)}
」と書き換えてみましょう。
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 によってセミコロン ;
の省略は許容されます)。つまり、クリックのイベントと関数の発火が紐づきません。
イベントと関数が連携されていないのであれば、マスのクリックから処理が発生せず何も起きないただのバグになるはずです。つまり、アプリケーションの動作が停止するような致命的な問題とはなりえません。
それでは、次に実行結果を示します。
次のエラーが出力されてしまいます。
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)
を実行して...、と処理が永遠に続きます。
そこで、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」も表示できるように実装していきます。