React三目並べ
最終的な成果物を public フォルダ内の index.html に注入しています。
複数の子コンポーネントからデータを収集したい、あるいは 2 つの子コンポーネント同士で通信したい、と思ったら、代わりに親コンポーネントに共有の state を宣言するようにしてください。親コンポーネントはその state を子コンポーネントに prop 経由で渡すことができます。これにより、子同士および親子間で、コンポーネントが同期されるようになります。
props 子コンポーネントが親コンポーネントから受け取るもの
Board コンポーネントの state である squares が更新されたので、Board とそのすべての子が再レンダーされます。これにより、インデックス 0 である Square コンポーネントの value プロパティが null から X に変更されます
import { useState } from 'react';
function Square({value, onSquareClick }) {
console.log('clicked!')
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 になるのは、handleClick 関数の使い方が原因です。具体的には、handleClick 関数をボタンの onClick プロパティに渡す際に、関数の呼び出し(handleClick(0) など)をしてしまっているためです。
() => handleClick(0) のように無名関数を使うことで、ボタンがクリックされるまで handleClick 関数は呼び出されません。これにより、handleClick が即座に実行されることを防ぎます。とのことですがhandleClick(0)としてしまうとbuttonタグがブラウザに認識されたタイミングで実行されてしまうということですか。
はい、その通りです。
import { useState } from 'react';
export default function Board() {
const [states, setState] = useState(Array(9).fill(null));
function handleClick(i) {
states[i] = 'x'
}
return (
<>
<div className="borad-row">
<button onClick={handleClick(0)}>{states[0]}</button>
<button onClick={handleClick(1)}>{states[1]}</button>
<button onClick={handleClick(2)}>{states[2]}</button>
</div>
<div className="borad-row">
<button onClick={handleClick(3)}>{states[3]}</button>
<button onClick={handleClick(4)}>{states[4]}</button>
<button onClick={handleClick(5)}>{states[5]}</button>
</div>
<div className="borad-row">
<button onClick={handleClick(6)}>{states[6]}</button>
<button onClick={handleClick(7)}>{states[7]}</button>
<button onClick={handleClick(8)}>{states[8]}</button>
</div>
</>
)
}
これだと即時呼び出しになってしまっているので
最初からxが全てのところに入ってしまっている状態となる。
クリックしてもボタンが 'x' にならない原因は、handleClick 関数が状態を直接変更しているためです。React では、状態を直接変更するのではなく、setState を使って状態を更新する必要があります。状態を直接変更すると、React が状態の変更を検出せず、再レンダリングが行われないからです。
問題の詳細
あなたのコードでは、handleClick 関数が状態を直接変更しています:
function handleClick(i) {
states[i] = 'x'; // 状態を直接変更している
}
この状態変更は、React の状態管理システムに正しく認識されません。その結果、setState を使わないと、React は状態が変更されたことを認識せず、コンポーネントが再レンダリングされません。
import { useState } from 'react';
export default function Board() {
const [states, setState] = useState(Array(9).fill(null));
function handleClick(i) {
states[i] = 'x'
}
return (
<>
<div className="borad-row">
<button onClick={() => handleClick(0)}>{states[0]}</button>
<button onClick={() => handleClick(1)}>{states[1]}</button>
<button onClick={() => handleClick(2)}>{states[2]}</button>
</div>
<div className="borad-row">
<button onClick={() => handleClick(3)}>{states[3]}</button>
<button onClick={() => handleClick(4)}>{states[4]}</button>
<button onClick={() => handleClick(5)}>{states[5]}</button>
</div>
<div className="borad-row">
<button onClick={() => handleClick(6)}>{states[6]}</button>
<button onClick={() => handleClick(7)}>{states[7]}</button>
<button onClick={() => handleClick(8)}>{states[8]}</button>
</div>
</>
)
}
修正方法
状態を変更するには、setState 関数を使って新しい状態を設定する必要があります。具体的には、以下の手順で修正します:
状態のコピーを作成:
現在の状態 (states) を変更するのではなく、コピーを作成して変更します。
状態の更新:
コピーした状態を setState を使って更新します。
以下のように修正してください:
コードをコピーする
import { useState } from 'react';
export default function Board() {
const [states, setStates] = useState(Array(9).fill(null));
function handleClick(i) {
// 状態のコピーを作成
const nextStates = states.slice();
if (nextStates[i] === null) { // すでに値が設定されていない場合のみ変更
nextStates[i] = 'x';
setStates(nextStates); // 状態を更新
}
}
return (
<>
<div className="board-row">
<button onClick={() => handleClick(0)}>{states[0]}</button>
<button onClick={() => handleClick(1)}>{states[1]}</button>
<button onClick={() => handleClick(2)}>{states[2]}</button>
</div>
<div className="board-row">
<button onClick={() => handleClick(3)}>{states[3]}</button>
<button onClick={() => handleClick(4)}>{states[4]}</button>
<button onClick={() => handleClick(5)}>{states[5]}</button>
</div>
<div className="board-row">
<button onClick={() => handleClick(6)}>{states[6]}</button>
<button onClick={() => handleClick(7)}>{states[7]}</button>
<button onClick={() => handleClick(8)}>{states[8]}</button>
</div>
</>
);
}
修正内容の説明
nextStates の作成:
states.slice() を使用して、現在の状態のコピー (nextStates) を作成します。これにより、状態の不変性が保たれます。
条件付き更新:
if (nextStates[i] === null) の条件を追加して、ボタンがまだ 'x' でない場合にのみ更新します。これにより、既に値が設定されているボタンのクリックによる変更を防ぎます。
setStates の呼び出し:
setStates(nextStates) を呼び出して、新しい状態 (nextStates) を設定します。これにより、React は状態の変更を認識し、コンポーネントが再レンダリングされます。
この修正により、クリックしたボタンだけが 'x' になるように機能します。
setState関数はsetState(設定したい値)の形しか取れないのですか。
setState 関数は、状態を更新するためにさまざまな形式を取ることができます。以下の2つの主な方法があります:
- 新しい状態を直接指定する
最も一般的な使い方で、setState に新しい状態の値を直接指定します。例えば:
setState(newState);
ここで newState は状態として設定したい新しい値です。
- 状態の更新関数を渡す
この方法では、現在の状態を基に新しい状態を計算する関数を setState に渡します。これにより、前の状態に基づいた更新が可能です。例えば:
コードをコピーする
setState(prevState => {
// prevState は現在の状態
return updatedState; // 更新された新しい状態を返す
});
これにより、状態が非同期的に更新される場合でも、最新の状態を基にして正しい状態を設定できます。
例
以下に、2つの方法を示します。
- 新しい状態を直接指定
コードをコピーする
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
- 状態の更新関数を使用
コードをコピーする
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(prevCount => prevCount + 1)}>Increment</button>
</div>
);
}
まとめ
直接指定: 状態の新しい値を直接 setState に渡します。
更新関数: 現在の状態に基づいて新しい状態を計算する関数を setState に渡します。これにより、状態の非同期更新に対処できます。
どちらの方法も状況に応じて使い分けることができますが、状態が前の状態に依存する場合は、更新関数を使用する方が安全です。
サイレンダリングが行われるタイミングについて
Array.prototype.map()
メソッドのコールバック関数には、2つの引数が渡されます。具体的には次のようになります:
const moves = history.map((squares, move) => {
// コールバック関数の中で、squares と move を使用します
});
引数の説明
-
第一引数 (
squares
):- 現在の配列の各要素を表します。
map()
メソッドを呼び出す配列の各要素がこの引数に渡されます。ここでは、history
配列の各要素(つまり、ゲームボードの状態)がsquares
に格納されます。
- 現在の配列の各要素を表します。
-
第二引数 (
move
):- 現在処理している要素のインデックス(位置)を表します。配列内でその要素がどの位置にあるかを示します。これにより、
map()
メソッド内でその要素のインデックスを知ることができます。
- 現在処理している要素のインデックス(位置)を表します。配列内でその要素がどの位置にあるかを示します。これにより、
例
例えば、history
が以下のような配列であったとします:
const history = [
[null, null, null, null, null, null, null, null, null], // 初期状態
[null, 'X', null, null, null, null, null, null, null], // 1手目
[null, 'X', 'O', null, null, null, null, null, null] // 2手目
];
map()
を使ってこれを処理する場合、次のようになります:
const moves = history.map((squares, move) => {
console.log(squares); // 現在の状態(各ゲームボード)
console.log(move); // 現在のインデックス
});
出力は次のようになります:
-
最初の呼び出し (
move
が 0 のとき):-
squares
:[null, null, null, null, null, null, null, null, null]
-
move
:0
-
-
2回目の呼び出し (
move
が 1 のとき):-
squares
:[null, 'X', null, null, null, null, null, null, null]
-
move
:1
-
-
3回目の呼び出し (
move
が 2 のとき):-
squares
:[null, 'X', 'O', null, null, null, null, null, null]
-
move
:2
-
このように、map()
メソッドのコールバック関数には、配列の要素とその要素のインデックスの両方が渡されます。
最初の currentMove + 1 要素を取り出します。とのことですが一番手の場合はcurrentMove = 0となり存在しないindex1を取り出すことになりませんか
良い指摘です。currentMove
が 0
の場合に history.slice(0, currentMove + 1)
がどう動作するのかについて説明します。
currentMove
が 0
の場合の slice
の動作
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
具体的な動作
-
currentMove
が0
の場合:-
currentMove + 1
は1
です。 -
history.slice(0, 1)
はhistory
配列のインデックス0
から1
までの要素を取得します(1
は含まないので、インデックス0
の要素のみが取得されます)。
-
-
history.slice(0, 1)
の結果:-
history
が[[null, null, null, null, null, null, null, null, null]]
であれば、history.slice(0, 1)
は[ [null, null, null, null, null, null, null, null, null] ]
になります。
-
-
nextHistory
の生成:-
nextHistory
は、history.slice(0, currentMove + 1)
で取得した履歴(ここでは[[null, null, null, null, null, null, null, null, null]]
)にnextSquares
を追加した配列です。 - つまり、
nextHistory
は[ [null, null, null, null, null, null, null, null, null], nextSquares ]
になります。
-
結論
currentMove
が 0
の場合でも、history.slice(0, currentMove + 1)
は空でない配列を返します。具体的には、[history[0]]
が返され、そこに nextSquares
が追加されます。この操作は正常に履歴を更新するためのものです。
全体の流れ
-
初期の
history
:history = [[null, null, null, null, null, null, null, null, null]];
-
handlePlay
が呼ばれ、nextSquares
が次の状態を示す:nextSquares = ['X', null, null, null, null, null, null, null, null];
-
nextHistory
の生成:nextHistory = [[null, null, null, null, null, null, null, null, null], ['X', null, null, null, null, null, null, null, null]];
-
setHistory(nextHistory)
:- これにより、履歴が更新され、新しい状態が履歴に追加されます。
-
setCurrentMove(nextHistory.length - 1)
:-
nextHistory.length - 1
は新しい履歴の最後のインデックス(ここでは1
)で、現在の手を示します。
-
-
setXIsNext(!xIsNext)
:- プレイヤーを切り替えます。
まとめ
history.slice(0, currentMove + 1)
は、currentMove
が 0
の場合でも history
配列の最初の要素を取得します。これにより、履歴に新しい状態を追加するための nextHistory
が正しく生成されます。slice
の終了インデックスは 1
で、配列の最初の要素のみが取得されます。
handlePlay
関数内での nextHistory
の動作について説明します。特に、ゲームの最初の手(currentMove
が 0 の場合)に nextHistory
がどうなるかに注目します。
handlePlay
関数の動作
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
setXIsNext(!xIsNext);
}
currentMove
が 0 の場合)
状況: 最初の手 (-
history
の状態:- 最初の手では、
history
は[Array(9).fill(null)]
です。この配列にはゲームの初期状態が含まれています。
- 最初の手では、
-
nextSquares
の内容:-
nextSquares
は現在の手を反映した新しいゲーム状態です。例えば、プレイヤーX
が最初の手で0
番目のボードの位置に置いた場合、nextSquares
は[ 'X', null, null, null, null, null, null, null, null ]
になります。
-
-
nextHistory
の生成:-
history.slice(0, currentMove + 1)
はhistory
の最初のcurrentMove + 1
要素を取り出します。currentMove
が 0 の場合、これは[Array(9).fill(null)]
です。 -
nextHistory
はこれにnextSquares
を追加した配列です。結果として、nextHistory
は[Array(9).fill(null), nextSquares]
になります。
-
-
setHistory(nextHistory)
:-
history
をnextHistory
に更新します。これにより、履歴に新しい状態が追加されます。
-
-
setCurrentMove(nextHistory.length - 1)
:-
nextHistory.length
は履歴の長さです。最初の手ではnextHistory
の長さは 2 です。setCurrentMove
にはnextHistory.length - 1
、つまり 1 が渡されます。これにより、現在の手のインデックスが 1 に設定されます。
-
-
setXIsNext(!xIsNext)
:- プレイヤーのターンを切り替えます。最初の手が終わった後、
X
の次にO
がプレイするため、xIsNext
の状態が反転します。
- プレイヤーのターンを切り替えます。最初の手が終わった後、
X
が 0
番目の位置に置いた場合
例: 最初の手で -
初期の
history
:history = [[null, null, null, null, null, null, null, null, null]];
-
nextSquares
:nextSquares = ['X', null, null, null, null, null, null, null, null];
-
生成される
nextHistory
:nextHistory = [[null, null, null, null, null, null, null, null, null], ['X', null, null, null, null, null, null, null, null]];
-
履歴が更新されると:
setHistory(nextHistory); setCurrentMove(1); setXIsNext(false); // 次は `O` のターン
このように、handlePlay
関数はゲームの履歴を更新し、次のターンの状態を管理します。最初の手では nextHistory
に新しいゲーム状態が追加され、setCurrentMove
で現在の手が 1 に更新され、次にプレイするプレイヤーが O
に変更されます。