茶葉置き場

IT関係のあれこれ(予定)

Reactのチュートリアルをやってみた

これまで仕事ではサーバーサイドが中心だったので、フロント技術を少し触れてみようかと。
今回は仕事のフロントサイドで使用されていたReactを選択。
公式サイト(↓)で三目並べを作るチュートリアルと練習問題が用意されていたので、とりあえずやってみた。

チュートリアル:三目並べ



チュートリアルを終えての感想

  • 現状、思っていたよりは難しくない

    仕事で障害対応の時に何となく見ていたので案外スッと理解できた。
    「パーツを作って組み合わせて画面を作る」と聞いていて、なんだそれ状態だったが意味が分かった。
    サーバーでプライベート関数作って処理を切り分けるのと同じイメージ。
    他の箇所で再利用しやすくなるし、同じコードを何回も書く手間を減らせて便利だなと感じた。

  • とは言え、当然難しい部分もある

    コンポーネントの適切な切り分け。
     ⇒慣れもあるだろうが、現状ではどこで分離すればいいのか判断がつかない。
    useStateの使いどころ。
     ⇒色々操作して再レンダリングが実行される中でも値を保持したい場合に使う?
    HTMLの部分とJavaScriptの部分が混在するので読みにくい。
     ⇒完成画面の全体像が把握できてないと、どこがどこに当たるのか理解するのが大変。
      チュートリアルですら難しいなーと感じたので、
      未経験で入社して研修上がりでいきなりReactでの開発だったら挫折する人いそう。
    型が無いのがしんどい。
     ⇒これはReactというよりJavaScriptの話なのでTypeScriptと組み合わせれば解決できそうだが。

  • これから
    まだ公式サイトに学習コンテンツがあるので一通り見てみようと思う。
    その後はReactフロント、C#サーバーで何か簡単なWebアプリが作てみようかなぁ。


■作成したコード

自作したコード

  • 一応練習問題含めて、動くものはできた
  • コメントがスカスカだったり、二重の三項演算子がある等の問題はあるが自力で組めたので大甘判定で良しとする
import { useState } from "react";

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

function SquareSet({ xIsNext, squares, onPlay, winLine }) {
  function handleClick(i) {
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    const nextSquares = squares.slice();
    if (xIsNext) {
      nextSquares[i] = "X";
    } else {
      nextSquares[i] = "O";
    }
    onPlay(nextSquares);
  }

  // 縦3列のマスを作成
  const heights = [...Array(3).keys()].map((i) => i * 3);
  // 縦の開始Noに従って横3列を作成
  return heights.map((height) => {
    const widths = [...Array(3).keys()].map((i) => height + i);
    const squareList = widths.map((i) => {
      const color = winLine && winLine.includes(i) ? "orange" : "white";
      return (
        <Square
          value={squares[i]}
          onSquareClick={() => handleClick(i)}
          color={color}
          key={i}
        />
      );
    });

    return (
      <div className="board-row" key={height}>
        {squareList}
      </div>
    );
  });
}

function Board({ xIsNext, squares, onPlay }) {
  const winLine = calculateWinner(squares);
  const status = winLine
    ? "Winner: " + (xIsNext ? "O" : "X")
    : squares.includes(null)
    ? "Next player: " + (xIsNext ? "X" : "O")
    : "引き分け!";

  return (
    <>
      <div className="status">{status}</div>
      <SquareSet
        xIsNext={xIsNext}
        squares={squares}
        onPlay={onPlay}
        winLine={winLine}
      />
    </>
  );
}

export default function Game() {
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [currentMove, setCurrentMove] = useState(0);
  const xIsNext = currentMove % 2 === 0;
  const currentSquares = history[currentMove];
  const [isAsc, setIsAsc] = useState(true);

  function handlePlay(nextSquares) {
    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
    setHistory(nextHistory);
    setCurrentMove(nextHistory.length - 1);
  }

  function jumpTo(nextMove) {
    setCurrentMove(nextMove);
  }

  function changeOrder() {
    setIsAsc(!isAsc);
  }

  function createMoves() {
    let beforeSquare = [Array(9).fill(null)];
    const moves = history.map((squares, move) => {
      const rowCal = findRowCal(beforeSquare, squares);
      beforeSquare = squares.slice();

      const description =
        move > 0
          ? "Go to move #" + move + "、設置場所:" + rowCal
          : "Go to game start";

      if (move !== history.length - 1) {
        return (
          <li key={move}>
            <button onClick={() => jumpTo(move)}>{description}</button>
          </li>
        );
      } else {
        return (
          <li key={move}>
            <div className="status">{"You are at move #" + move}</div>
          </li>
        );
      }
    });

    return isAsc ? moves : moves.reverse();
  }

  const moves = createMoves();
  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{moves}</ol>
        <button onClick={() => changeOrder()}>昇順・降順切り替え</button>
      </div>
    </div>
  );
}

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return lines[i];
    }
  }
  return null;
}

function findRowCal(beforeSquare, nowSquare) {
  const rowCalSet = [
    "row: 1, cal: 1",
    "row: 1, cal: 2",
    "row: 1, cal: 3",
    "row: 2, cal: 1",
    "row: 2, cal: 2",
    "row: 2, cal: 3",
    "row: 3, cal: 1",
    "row: 3, cal: 2",
    "row: 3, cal: 3"
  ];

  const changeIndex = beforeSquare.findIndex(
    (x, index) => x === null && nowSquare[index] !== null
  );
  return changeIndex < 0 ? "" : rowCalSet[changeIndex];
}

ChatGPTで改善点を聞いてみて回答結果のコード

  • せっかくなのでChatGPTにコードを貼り付けて改善点を聞いてみた
  • 挙がってきた改善点(改善例は下記のコード)

    1. Array.slice()をスプレッド構文([...Array])で行う
    2. 縦、横のマスを作成する処理を1関数に分離し、配列で実行するように変更
    3. 文字列結合を~~ ${~~~}で行う
    4. 関数内で定義してる関数を外出しする(handleClick など)
  • 大きく変わったのが2だけなので、全体としてはそこまで悪いコードではなかったと思いたい

  • ちなみにこれをそのまま実行するとバグっており、正常動作しなかった
  • 雑に指示しただけなので、もっと細かく指示すればちゃんと動くコードも作れそう
import { useState } from "react";

function Square({ value, onClick, color }) {
  return (
    <button
      className="square"
      onClick={onClick}
      style={{ backgroundColor: color }}
    >
      {value}
    </button>
  );
}

function BoardRow({ xIsNext, squares, onPlay, winLine }) {
  function handleClick(i) {
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    const nextSquares = [...squares];
    nextSquares[i] = xIsNext ? "X" : "O";
    onPlay(nextSquares);
  }

  const renderSquare = (i) => {
    const color = winLine && winLine.includes(i) ? "orange" : "white";
    return (
      <Square
        value={squares[i]}
        onClick={() => handleClick(i)}
        color={color}
        key={i}
      />
    );
  };

  return <div className="board-row">{[0, 1, 2].map(renderSquare)}</div>;
}

function Board({ xIsNext, squares, onPlay }) {
  const winLine = calculateWinner(squares);
  const status = winLine
    ? `Winner: ${xIsNext ? "O" : "X"}`
    : squares.includes(null)
    ? `Next player: ${xIsNext ? "X" : "O"}`
    : "引き分け!";

  return (
    <>
      <div className="status">{status}</div>
      {[0, 3, 6].map((start) => (
        <BoardRow
          key={start}
          xIsNext={xIsNext}
          squares={squares.slice(start, start + 3)}
          onPlay={onPlay}
          winLine={winLine}
        />
      ))}
    </>
  );
}

export default function Game() {
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [currentMove, setCurrentMove] = useState(0);
  const xIsNext = currentMove % 2 === 0;
  const currentSquares = history[currentMove];
  const [isAsc, setIsAsc] = useState(true);

  function handlePlay(nextSquares) {
    setHistory((prevHistory) => [...prevHistory, nextSquares]);
    setCurrentMove((prevMove) => prevMove + 1);
  }

  function jumpTo(move) {
    setCurrentMove(move);
  }

  function changeOrder() {
    setIsAsc((prevIsAsc) => !prevIsAsc);
  }

  function createMoves() {
    let beforeSquare = Array(9).fill(null);
    const moves = history.map((squares, move) => {
      const rowCal = findRowCal(beforeSquare, squares);
      beforeSquare = squares.slice();

      const description =
        move > 0
          ? `Go to move #${move}、設置場所:${rowCal}`
          : "Go to game start";

      if (move !== history.length - 1) {
        return (
          <li key={move}>
            <button onClick={() => jumpTo(move)}>{description}</button>
          </li>
        );
      } else {
        return (
          <li key={move}>
            <div className="status">{"You are at move #" + move}</div>
          </li>
        );
      }
    });

    return isAsc ? moves : moves.reverse();
  }

  const moves = createMoves();

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{moves}</ol>
        <button onClick={changeOrder}>昇順・降順切り替え</button>
      </div>
    </div>
  );
}

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return lines[i];
    }
  }
  return null;
}

function findRowCal(beforeSquare, nowSquare) {
  const rowCalSet = [
    "row: 1, cal: 1",
    "row: 1, cal: 2",
    "row: 1, cal: 3",
    "row: 2, cal: 1",
    "row: 2, cal: 2",
    "row: 2, cal: 3",
    "row: 3, cal: 1",
    "row: 3, cal: 2",
    "row: 3, cal: 3"
  ];

  const changeIndex = beforeSquare.findIndex(
    (x, index) => x === null && nowSquare[index] !== null
  );
  return changeIndex < 0 ? "" : rowCalSet[changeIndex];
}

最終的なコード

  • ChatGPTの回答を参考に動くように一部修正したもの
  • renderSquareの処理がスッキリして分かりやすくなった
  • ただし、引数がstartIndex + iになったりしていて3*3マスであることは分かりにくくなった印象
  • コメント入れたり、配列作る時の数字を定数化すれば許容範囲かな、というところ
  • 二重の三項演算子がある等、気になる点はまだあるが今回はこれを最終とする
import { useState } from "react";

function Square({ value, onClick, color }) {
  return (
    <button
      className="square"
      onClick={onClick}
      style={{ backgroundColor: color }}
    >
      {value}
    </button>
  );
}

function BoardRow({ xIsNext, squares, onPlay, winLine, startIndex }) {
  function handleClick(i) {
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    const nextSquares = [...squares];
    nextSquares[i] = xIsNext ? "X" : "O";
    onPlay(nextSquares);
  }

  const renderSquare = (i) => {
    const color = winLine && winLine.includes(i) ? "orange" : "white";
    return (
      <Square
        value={squares[i]}
        onClick={() => handleClick(i)}
        color={color}
        key={i}
      />
    );
  };

  return (
    <div className="board-row">
      {[...Array(3).keys()].map((i) => renderSquare(startIndex + i))}
    </div>
  );
}

function Board({ xIsNext, squares, onPlay }) {
  const winLine = calculateWinner(squares);
  const status = winLine
    ? `Winner: ${xIsNext ? "O" : "X"}`
    : squares.includes(null)
    ? `Next player: ${xIsNext ? "X" : "O"}`
    : "引き分け!";

  return (
    <>
      <div className="status">{status}</div>
      {[...Array(3).keys()].map((start) => (
        <BoardRow
          key={start}
          xIsNext={xIsNext}
          squares={squares}
          onPlay={onPlay}
          winLine={winLine}
          startIndex={start * 3}
        />
      ))}
    </>
  );
}

export default function Game() {
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [currentMove, setCurrentMove] = useState(0);
  const xIsNext = currentMove % 2 === 0;
  const currentSquares = history[currentMove];
  const [isAsc, setIsAsc] = useState(true);

  function handlePlay(nextSquares) {
    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
    setHistory(nextHistory);
    setCurrentMove(nextHistory.length - 1);
  }

  function jumpTo(move) {
    setCurrentMove(move);
  }

  function changeOrder() {
    setIsAsc(!isAsc);
  }

  function createMoves() {
    let beforeSquare = Array(9).fill(null);
    const moves = history.map((squares, move) => {
      const rowCal = findRowCal(beforeSquare, squares);
      beforeSquare = squares.slice();

      const description =
        move > 0
          ? `Go to move #${move}、設置場所:${rowCal}`
          : "Go to game start";

      if (move !== history.length - 1) {
        return (
          <li key={move}>
            <button onClick={() => jumpTo(move)}>{description}</button>
          </li>
        );
      } else {
        return (
          <li key={move}>
            <div className="status">{"You are at move #" + move}</div>
          </li>
        );
      }
    });

    return isAsc ? moves : moves.reverse();
  }

  const moves = createMoves();

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{moves}</ol>
        <button onClick={() => changeOrder()}>昇順・降順切り替え</button>
      </div>
    </div>
  );
}

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return lines[i];
    }
  }
  return null;
}

function findRowCal(beforeSquare, nowSquare) {
  const rowCalSet = [
    "row: 1, cal: 1",
    "row: 1, cal: 2",
    "row: 1, cal: 3",
    "row: 2, cal: 1",
    "row: 2, cal: 2",
    "row: 2, cal: 3",
    "row: 3, cal: 1",
    "row: 3, cal: 2",
    "row: 3, cal: 3"
  ];

  const changeIndex = beforeSquare.findIndex(
    (x, index) => x === null && nowSquare[index] !== null
  );
  return changeIndex < 0 ? "" : rowCalSet[changeIndex];
}