Reactを独学で学ぶ上で、いちばん最初に公式ドキュメントの理解から着手しようと思ったのでアウトプットも兼ねて内容をまとめました。
ここではReact公式ドキュメントの「チュートリアル:三目並べ」をまとめました。
こんな人向け
- Reactを触ったことがない人
- Reactに興味があり、学ぼうとしている
- Reactの概念・仕組みを知りたい
筆者の状態
- html, css, javaScriptはある程度知っている
- Reactは初心者
チュートリアル:三目並べ
チュートリアルで作成するもの
以下、チュートリアルで作成する完成物の三目並べゲームになります。
import { useState } from 'react';
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = 'X';
} else {
nextSquares[i] = 'O';
}
onPlay(nextSquares);
}
const winner = calculateWinner(squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (xIsNext ? 'X' : 'O');
}
return (
<>
<div className="status">{status}</div>
<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>
</>
);
}
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];
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
}
function jumpTo(nextMove) {
setCurrentMove(nextMove);
}
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = 'Go to move #' + move;
} else {
description = 'Go to game start';
}
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</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 squares[a];
}
}
return null;
}
チュートリアルを作成する前に、まずは上記のコードを動作させて動きを確認してみるのもよいと思います。
チュートリアルのセットアップ
Reactの公式サイトでは、CodeSandbox
を使用してブラウザ上でコードを書きながら画面表示の確認もできるようになっているので、そちらの方が楽だと思う方はReact公式サイトからチュートリアル作成を行ってください。
ここではローカルにReactプロジェクトを作成して、チュートリアルを実施していきたいと思います。
■Reactプロジェクトの作成
プロジェクトを作成したいローカルディレクトリに移動して
npm create vite@latest
...
Project name:
tutorial
Select a framework:
React
Select a variant:
TypeScript
tutorialフォルダに移動して、npm i
cd tutorial
npm i
これでローカルにReactプロジェクトを作成することが出来ました。
index.cssを下記のようにしましょう。
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
margin: 20px;
padding: 0;
}
h1 {
margin-top: 0;
font-size: 22px;
}
h2 {
margin-top: 0;
font-size: 20px;
}
h3 {
margin-top: 0;
font-size: 18px;
}
h4 {
margin-top: 0;
font-size: 16px;
}
h5 {
margin-top: 0;
font-size: 14px;
}
h6 {
margin-top: 0;
font-size: 12px;
}
code {
font-size: 1.2em;
}
ul {
padding-inline-start: 20px;
}
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
margin: 20px;
padding: 0;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.board-row:after {
clear: both;
content: "";
display: table;
}
.status {
margin-bottom: 10px;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
App.tsxを下記のようにしましょう。
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";
export default function Square() {
return <button className="square">X</button>;
}
App.tsxにあるコードはコンポーネントを作成します。ReactのコンポーネントとはUIの部品を表す再利用可能なコードのことです。コンポーネントは、アプリケーションのUI要素を表示し、管理し、更新するために使用します。
App.tsxを見ると
export default function Square() { //ここ
return <button className="square">X</button>;
}
export
というキーワードは、この関数をこのファイルの外部からアクセスできるようにします。default
キーワードは、このコードを使用する他のファイルに、これがこのファイルのメイン関数であるということを伝えます。
export default function Square() {
return <button className="square">X</button>; //ここ
}
<button>
はJSX要素(JSX element)と呼ばれます。JSX要素とは、何を表示したいかを記述するためのJavaScriptコードとHTMLタグの組み合わせです。classNmae="square"
はこのボタンのプロパティ、またはpropsと呼ばれるもので、CSSにボタンをどのようにスタイル付けするか伝えます。
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
1~5行目の説明をします。
- React
- Webブラウザとやり取りするためのReactライブラリ(React DOM)
- コンポーネント用のスタイル
-
App.tsx
で作成したコンポーネント
盤面の作成
App.tsxに戻ります。
現在の盤面(board)にはマス目(square)が1つしかありませんが、本来は9つ必要です。
ですがReactコンポーネントからは、JSX要素を複数隣り合わせて返すのではなく、単一のJSX要素を返す必要があります。
なので複数のJSX要素は以下のように(<>
および </>
)で囲むようにします。
export default function Square() {
return (
<>
<button className="square">X</button>
<button className="square">X</button>
</>
);
}
これをグリッド状に並べたいので、<div>
を使用してグループ分けを行います。
export default function Square() {
return (
<>
<div className="board-row">
<button className="square">1</button>
<button className="square">2</button>
<button className="square">3</button>
</div>
<div className="board-row">
<button className="square">4</button>
<button className="square">5</button>
<button className="square">6</button>
</div>
<div className="board-row">
<button className="square">7</button>
<button className="square">8</button>
<button className="square">9</button>
</div>
</>
);
}
ここでSquare
関数の名前をBoard
に変えましょう。
propsを通してデータを渡す
button
タグのコピーではごちゃごちゃしているのでコンポーネントの再利用で実装してみましょう。
新しく Square
関数を作成します。
function Square() {
return <button className="square">1</button>;
}
export default function Board() {
// ...
}
次に、Board
コンポーネントを更新し、JSX構文を利用してこの Square
コンポーネントをレンダーしましょう。
// ...
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
コンポーネントは大文字で始める必要があることに注意してください。
propsとして Board
コンポーネントから Square
コンポーネントに渡して
1-9までのマス目を作成していきます。
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";
function Square({ value }) {
return <button className="square">{value}</button>;
}
export default function Board() {
return (
<>
<div className="board-row">
<Square value="1" />
<Square value="2" />
<Square value="3" />
</div>
<div className="board-row">
<Square value="4" />
<Square value="5" />
<Square value="6" />
</div>
<div className="board-row">
<Square value="7" />
<Square value="8" />
<Square value="9" />
</div>
</>
);
}
インタラクティブなコンポーネントの作成
では Square
コンポーネントをクリックすると X
が表示されるようにしてみましょう。
まず、Square
コンポーネントに、クリックされたことを「記憶」して"X"マークを表示してもらいます。何かを「記録」するために、コンポーネントはstateというのものを使用します。
Reactの useState
を使用して Square
の現在の値をstateに保存し、Square
がクリックされたときにその値を変更しましょう。
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue("X");
}
return (
<button className="square" onClick={handleClick}>
{value}
</button>
);
}
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
各Squareはそれぞれ独自のstateを保持しています。それぞれのSquareに格納されている value
は、他のモノとは完全に独立しています。コンポーネントの set
関数を呼び出すと、Reactは自動的に内部にある子コンポーネントも更新します。
ゲームを完成させる
ここまでの作業で、三目並べゲームの基本的な土台は完成しました。
ゲーム自体を完成さえるために"X"と"〇"を置けるようにすることと、勝ち負けのロジックを追加します。
Stateのリフトアップ
Board
コンポーネントは各 Square
のどの位置にマークが置かれているか知る必要があります。ですので親コンポーネント Board
に共有のstateを宣言し、子コンポーネント Square
にprops経由で渡します。
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";
function Square({ value }) {
return <button className="square">{value}</button>;
}
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<>
<div className="board-row">
<Square value={squares[0]} />
<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>
</>
);
}
すでに Square
から Board
のstateを変更することはできないので、Board
コンポーネントから Square
コンポーネントに関数を渡して、マス目がクリックされたときに Square
にその関数を呼び出してもらうようにします。
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.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() {
const nextSquares = squares.slice();
nextSquares[0] = "X";
setSquares(nextSquares);
}
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={handleClick} />
<Square value={squares[1]} onSquareClick={handleClick} />
<Square value={squares[2]} onSquareClick={handleClick} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={handleClick}/>
<Square value={squares[4]} onSquareClick={handleClick}/>
<Square value={squares[5]} onSquareClick={handleClick}/>
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={handleClick}/>
<Square value={squares[7]} onSquareClick={handleClick}/>
<Square value={squares[8]} onSquareClick={handleClick}/>
</div>
</>
);
}
handleClick
関数は、slice()
配列メソッドを使って squares
配列のコピー( nextSquares
)を作成します。※slice
は引数が0だとコピーを作成する
次に、
handleClick
は、nextSquares
配列を更新して最初の(インデックス[0]
の)マス目にX
と書き込みます。
seSquares
関数をコールすることで、Reactはこのコンポーネントのstateに変更があったことを知ります。これにより、squares
というstate変数を使用しているコンポーネントBoard
、およびその子コンポーネント(盤面を構成しているSquare
コンポーネントすべて)の再レンダーがトリガされます。
任意のマス目を変更できるようにすると
function handleClick(i) {
const nextSquares = squares.slice();
nextSquares[i] = "X";
setSquares(nextSquares);
}
次に、その i
を handleClick
に渡す必要があります。以下のように、JSX内で直接渡したくなりますが、これはうまくいきません。
<Square value={squares[0]} onSquareClick={handleClick(0)} />
これがうまくいかない理由は、handleClick(0)
の呼び出しが、Boardコンポーネントのレンダーの一部として発生してしまうからです。handleClick(0)
は、setSquares
を呼び出してBoardコンポーネントのstateを更新するため、Boardコンポーネント全体が再レンダーされます。これにより、handleClick(0)
が再度実行され、無限ループに陥ります。
しかし、propsとして(onSquareClick={handleClick}
)のように渡すと関数は実行されず呼び出されないので、無限ループにはなりません。
じゃあどうすればよいかというと、アロー関数を使います。
export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
// ...
);
}
export default function Board() {
// ...
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>
</>
);
};
state 管理が Board コンポーネントに移動されたので、親である Board コンポーネントは子の Square コンポーネントに props を渡すことで、それらが正しく表示されるようにしています。Square をクリックすると、子である Square コンポーネントが親である Board コンポーネントに盤面の state を更新するように要求します。Board の state が変更されると、Board コンポーネントとすべての Square 子コンポーネントが自動的に再レンダーされます。すべてのマス目の state を Board コンポーネントにまとめて保持しておくことで、この後でゲームの勝者を決めることが可能になります。
シーケンスを再確認
ユーザが盤面の左上のマス目をクリックして X を置いた場合を例に、何が起こるのかをおさらいしましょう。
左上のマス目をクリックすると、button が props として受け取った onClick 関数が実行されます。Square コンポーネントはその関数を Board から onSquareClick プロパティとして受け取っています。Board コンポーネントはその関数を JSX の中で直接定義しています。その関数は引数 0 で handleClick を呼び出します。
handleClick は引数 0 を使って、squares 配列の最初の要素を null から X に更新します。
Board コンポーネントの state である squares が更新されたので、Board とそのすべての子が再レンダーされます。これにより、インデックス 0 である Square コンポーネントの value プロパティが null から X に変更されます。
最終的に、クリックによって左上のマス目が空白から X に変わったという結果をユーザが目にすることになります。
なぜイミュータビリティが重要なのか
handleClick
内で既存の squares
配列を直接変更するのではなく、.slice()
を使ったのでしょうか?それはイミュータビリティ(不変性)という概念と
それが重要であるからです。
データを変更する方法は一般的に2つのアプローチがあります。
1つめのアプローチはデータの値を直接書き換える方法です。
const squares = [null, null, null, null, null, null, null, null, null];
squares[0] = 'X';
// Now `squares` is ["X", null, null, null, null, null, null, null, null];
一方で、以下が元データを書き換えない方法です。
const squares = [null, null, null, null, null, null, null, null, null];
const nextSquares = ['X', null, null, null, null, null, null, null, null];
// Now `squares` is unchanged, but `nextSquares` first element is 'X' rather than `null`
この利点は以下です。
- Reactがコンポーネントの変化を検知して再レンダーするため、無駄なレンダーを防ぎ、パフォーマンスの効率を上げるため
- 元のデータを再利用したい場合があるため
次に、"X"と"〇"を交互に置けるようにします。
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick(i) {
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
setSquares(nextSquares);
setXIsNext(!xIsNext);
}
このようになります。
今のままでは"X"が"〇"に上書きされてしまうので
function handleClick(i) {
if (squares[i]) {
return;
}
const nextSquares = squares.slice();
//...
}
すでにマークがあれば、戻るようにします。
勝者の宣言
勝者がどちらかのロジックを追加します。
Board
は勝者が決まればゲームを終了し、勝者を表示します。ゲーム中は次のプレイヤーを表示します。
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
export default function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick(i) {
if (squares[i] || calculateWinner(squares)) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
setSquares(nextSquares);
setXIsNext(!xIsNext);
}
const winner = calculateWinner(squares);
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
}
return (
<>
<div className="status">{status}</div>
<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>
</>
);
}
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 squares[a];
}
}
return null;
}
タイムトラベルの追加
巻き戻し要素を入れていきます。
着手の履歴を保持する
過去の squares
配列を、histroy
という名前の別配列に入れて、それを新たにstate変数として保持することにします。histroy
配列は、最初の着手から最新の着手まで、盤面のすべての状態を表現しており、以下のような形になります。
[
// Before first move
[null, null, null, null, null, null, null, null, null],
// After first move
[null, null, null, null, 'X', null, null, null, null],
// After second move
[null, null, null, null, 'X', null, null, null, 'O'],
// ...
]
もう一度stateをリフトアップ
新しいトップレベルのコンポーネント、Game
を作成して、過去の着手の一覧を表示するようにします。ゲームの履歴全体を保持するstateである history
はここに置きます。
history 状態を Game コンポーネントに配置することで、その子になる Board コンポーネントからは squares の state を削除できます。Square コンポーネントから Board コンポーネントに「state をリフトアップ」したときと同じように、Board からトップレベルの Game コンポーネントに state をリフトアップすることになります。これにより、Game コンポーネントは Board のデータを完全に制御し、history からの過去の盤面の状態を Board にレンダーさせることができます。
function Board() {
// ...
}
export default function Game() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<ol>{/*TODO*/}</ol>
</div>
</div>
);
}
過去の着手の表示
複数の履歴を表示するためにmap関数を使います。
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
onPlay(nextSquares);
}
//...
}
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
function jumpTo(nextMove) {}
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = "Go to move #" + move;
} else {
description = "Go to move start";
}
return (
<li>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}
ただ、今の状態では以下のエラーが出ます。
hook.js:608 Each child in a list should have a unique "key" prop.
keyを選ぶ
リストをレンダーすると、React はレンダーされたリストの各アイテムに関するとある情報を保持します。そのリストが更新されると、React は何が変更されたのかを判断する必要があります。例えばリストのアイテムを追加したのかもしれませんし、削除、並べ替え、あるいは項目の中身の更新を行ったのかもしれません。
次のような状況から:
<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>
以下に遷移したと想像してください:
<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>
人間がこれを読めばおそらく、カウントの数字が変わっていることに加えて、Alexa と Ben の順番が入れ替わり、Claudia が Alexa と Ben の間に挿入された、と言うことでしょう。しかし、Reactはコンピュータプログラムでありあなたの意図を理解できません。
なのでReactでは以下のように記述してあげる必要があります。
<li key={user.id}>
{user.name}: {user.taskCount} tasks left
</li>
Reactにおけるリストとkey
プロパティ
Reactでリストをレンダリングする際、各リスト項目にkey
という特別なプロパティを指定することが重要です。
-
key
の役割: Reactはkey
を使って、リスト内の各項目を識別します。リストが再レンダリングされる際、Reactはこのkey
を頼りに、以前のリストと比較して、どの項目が追加・削除・移動されたかを効率的に判断します。 -
Stateの維持:
key
が同じであれば、対応するコンポーネントは維持され、その内部状態(state)も保持されます。逆にkey
が変わると、古いコンポーネントは破棄され、新しいstateを持つ新しいコンポーネントが作成されます。 -
key
はReact内部用:key
はpropsのようには見えますが、子コンポーネントに直接渡されるわけではなく、Reactが更新を判断するために内部的に使用します。 -
適切な
key
の設定: 動的なリストでは、各項目を一意に識別できる安定したkey
(例:データのID)を設定することが強く推奨されます。適切なIDがない場合は、データ構造を見直すことも検討しましょう。 -
インデックスを
key
にするのは非推奨:key
を指定しない場合、Reactはエラーを出し、デフォルトで配列のインデックスを使用します。しかし、インデックスをkey
として使うと、リストの並び替えや項目の挿入・削除時に予期せぬ挙動やパフォーマンスの問題を引き起こす可能性があるため、ほとんどの場合推奨されません。 -
key
の一意性の範囲:key
はリスト全体で完全に一意である必要はなく、同じリスト内の兄弟要素間で一意であれば十分です。
タイムトラベルの実装
戻りたい時点のボタンを押下すると、履歴がその時点まで戻るようにします。
import { useState } from "react";
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
onPlay(nextSquares);
}
const winner = calculateWinner(squares);
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
}
return (
<>
<div className="status">{status}</div>
<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>
</>
);
}
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];
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
}
function jumpTo(nextMove) {
setCurrentMove(nextMove);
}
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = "Go to move #" + move;
} else {
description = "Go to move start";
}
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</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 squares[a];
}
}
return null;
}
お疲れ様でした。
こちらでチュートリアルは終了です。
Reactの公式を全て載せているわけではないので、あくまで自身のアウトプット用として書かせていただきました。
本記事は、Reactの公式ドキュメント(React 公式サイトへのリンク)を参考に、Creative Commons Attribution 4.0 International License (CC BY 4.0) の下で公開されている情報に基づいて作成されています。