Closed12

React三目並べ

saki yamamotosaki yamamoto

最終的な成果物を public フォルダ内の index.html に注入しています。

saki yamamotosaki yamamoto

複数の子コンポーネントからデータを収集したい、あるいは 2 つの子コンポーネント同士で通信したい、と思ったら、代わりに親コンポーネントに共有の state を宣言するようにしてください。親コンポーネントはその state を子コンポーネントに prop 経由で渡すことができます。これにより、子同士および親子間で、コンポーネントが同期されるようになります。

saki yamamotosaki yamamoto

props 子コンポーネントが親コンポーネントから受け取るもの

Board コンポーネントの state である squares が更新されたので、Board とそのすべての子が再レンダーされます。これにより、インデックス 0 である Square コンポーネントの value プロパティが null から X に変更されます

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

saki yamamotosaki yamamoto

あなたのコードで全てのボタンが 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が全てのところに入ってしまっている状態となる。

saki yamamotosaki yamamoto

クリックしてもボタンが '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>
    </>
  )
}
saki yamamotosaki yamamoto

修正方法
状態を変更するには、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' になるように機能します。

saki yamamotosaki yamamoto

setState関数はsetState(設定したい値)の形しか取れないのですか。

setState 関数は、状態を更新するためにさまざまな形式を取ることができます。以下の2つの主な方法があります:

  1. 新しい状態を直接指定する
    最も一般的な使い方で、setState に新しい状態の値を直接指定します。例えば:
setState(newState);

ここで newState は状態として設定したい新しい値です。

  1. 状態の更新関数を渡す
    この方法では、現在の状態を基に新しい状態を計算する関数を setState に渡します。これにより、前の状態に基づいた更新が可能です。例えば:
コードをコピーする
setState(prevState => {
  // prevState は現在の状態
  return updatedState; // 更新された新しい状態を返す
});

これにより、状態が非同期的に更新される場合でも、最新の状態を基にして正しい状態を設定できます。


以下に、2つの方法を示します。

  1. 新しい状態を直接指定
コードをコピーする
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>
  );
}
  1. 状態の更新関数を使用
コードをコピーする
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 に渡します。これにより、状態の非同期更新に対処できます。
どちらの方法も状況に応じて使い分けることができますが、状態が前の状態に依存する場合は、更新関数を使用する方が安全です。

saki yamamotosaki yamamoto

Array.prototype.map() メソッドのコールバック関数には、2つの引数が渡されます。具体的には次のようになります:

const moves = history.map((squares, move) => {
  // コールバック関数の中で、squares と move を使用します
});

引数の説明

  1. 第一引数 (squares):

    • 現在の配列の各要素を表します。map() メソッドを呼び出す配列の各要素がこの引数に渡されます。ここでは、history 配列の各要素(つまり、ゲームボードの状態)が squares に格納されます。
  2. 第二引数 (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);    // 現在のインデックス
});

出力は次のようになります:

  1. 最初の呼び出し (move が 0 のとき):

    • squares: [null, null, null, null, null, null, null, null, null]
    • move: 0
  2. 2回目の呼び出し (move が 1 のとき):

    • squares: [null, 'X', null, null, null, null, null, null, null]
    • move: 1
  3. 3回目の呼び出し (move が 2 のとき):

    • squares: [null, 'X', 'O', null, null, null, null, null, null]
    • move: 2

このように、map() メソッドのコールバック関数には、配列の要素とその要素のインデックスの両方が渡されます。

saki yamamotosaki yamamoto

最初の currentMove + 1 要素を取り出します。とのことですが一番手の場合はcurrentMove = 0となり存在しないindex1を取り出すことになりませんか

良い指摘です。currentMove0 の場合に history.slice(0, currentMove + 1) がどう動作するのかについて説明します。

currentMove0 の場合の slice の動作

const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];

具体的な動作

  1. currentMove0 の場合:

    • currentMove + 11 です。
    • history.slice(0, 1)history 配列のインデックス 0 から 1 までの要素を取得します(1 は含まないので、インデックス 0 の要素のみが取得されます)。
  2. 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] ] になります。
  3. 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 ] になります。

結論

currentMove0 の場合でも、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) は、currentMove0 の場合でも history 配列の最初の要素を取得します。これにより、履歴に新しい状態を追加するための nextHistory が正しく生成されます。slice の終了インデックスは 1 で、配列の最初の要素のみが取得されます。

saki yamamotosaki yamamoto

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 の場合)

  1. history の状態:

    • 最初の手では、history[Array(9).fill(null)] です。この配列にはゲームの初期状態が含まれています。
  2. nextSquares の内容:

    • nextSquares は現在の手を反映した新しいゲーム状態です。例えば、プレイヤー X が最初の手で 0 番目のボードの位置に置いた場合、nextSquares[ 'X', null, null, null, null, null, null, null, null ] になります。
  3. nextHistory の生成:

    • history.slice(0, currentMove + 1)history の最初の currentMove + 1 要素を取り出します。currentMove が 0 の場合、これは [Array(9).fill(null)] です。
    • nextHistory はこれに nextSquares を追加した配列です。結果として、nextHistory[Array(9).fill(null), nextSquares] になります。
  4. setHistory(nextHistory):

    • historynextHistory に更新します。これにより、履歴に新しい状態が追加されます。
  5. setCurrentMove(nextHistory.length - 1):

    • nextHistory.length は履歴の長さです。最初の手では nextHistory の長さは 2 です。setCurrentMove には nextHistory.length - 1、つまり 1 が渡されます。これにより、現在の手のインデックスが 1 に設定されます。
  6. setXIsNext(!xIsNext):

    • プレイヤーのターンを切り替えます。最初の手が終わった後、X の次に O がプレイするため、xIsNext の状態が反転します。

例: 最初の手で X0 番目の位置に置いた場合

  • 初期の 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 に変更されます。

このスクラップは2024/08/11にクローズされました