From 76157167399ece449d5d098619220f345c7617c4 Mon Sep 17 00:00:00 2001 From: Miyagoshi_Sota Date: Mon, 11 Aug 2025 10:39:37 +0900 Subject: [PATCH] add:web_for-hackathos text --- .../textbook/web/for-hackathons/01--setup.mdx | 96 + .../textbook/web/for-hackathons/02--react.mdx | 174 + .../textbook/web/for-hackathons/03--3moku.mdx | 5077 +++++++++++++++++ 3 files changed, 5347 insertions(+) create mode 100644 src/content/docs/textbook/web/for-hackathons/01--setup.mdx create mode 100644 src/content/docs/textbook/web/for-hackathons/02--react.mdx create mode 100644 src/content/docs/textbook/web/for-hackathons/03--3moku.mdx diff --git a/src/content/docs/textbook/web/for-hackathons/01--setup.mdx b/src/content/docs/textbook/web/for-hackathons/01--setup.mdx new file mode 100644 index 0000000..12265aa --- /dev/null +++ b/src/content/docs/textbook/web/for-hackathons/01--setup.mdx @@ -0,0 +1,96 @@ +--- +title: 環境構築 +description: このチャプターでは、快適に XCode/VSCode で快適な C 言語を書くための秘訣を教えます。 +slug: textbook/web/for-hackathons/setup + +--- + +import { Aside } from "@astrojs/starlight/components"; +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +### Node.js, npmのインストール + +1. [Node.js](https://nodejs.org/)の公式サイトにアクセスします。 +2. 画像のボタンをクリックすると、お使いのOSに適したLTS版インストールできるインストーラ(.pkg,.msi)がインストールされます。 + + + + 3. インストーラ(.pkg)をクリックすると以下の画像のウィンドウが現れるので順番にインストール処理を行います。 + + + + + + + + + + 3. インストーラ(.msi)をクリックすると以下の画像のウィンドウが現れるので順番にインストール処理を行います。 + + + + + + + + + + + + +4. インストール後、以下のコマンドでバージョンを確認できれば正常にインストールされています。表示されているバージョンが画像と同じでなくても大丈夫です。 + + ```sh + node -v + npm -v + ``` + + + +### Reactプロジェクトのスタート + +1. ターミナルまたはコマンドプロンプトを開き、以下のコマンドを実行します。\ + これにより、Viteを使用して新しいReactプロジェクトが作成されます。\ + ここでは、プロジェクト名を`my-app`としていますが、任意の名前に変更できます。 + + ```sh + npm create vite@latest my-app + ``` + + + + 1. Reactを選択します。 + + + 2. JavaScriptを選択します。 + + + 3. Doneが表示されたら完了です。 + + +2. 作成したプロジェクトのディレクトリに移動します。 +{/* タブ選択したい */} + + ```sh + cd my-app + ``` + + +3. 依存関係をインストールします。 + + ```sh + npm install + ``` + + + +4. 実行すると、開発サーバーが立ち上がり、`➜ Local: http://localhost:5173/`の`http://http://localhost:⚪︎⚪︎⚪︎⚪︎`にブラウザでアクセスすると、Reactアプリが確認できます。 + ```sh + npm run dev + ``` + + + + +--- \ No newline at end of file diff --git a/src/content/docs/textbook/web/for-hackathons/02--react.mdx b/src/content/docs/textbook/web/for-hackathons/02--react.mdx new file mode 100644 index 0000000..4a1caea --- /dev/null +++ b/src/content/docs/textbook/web/for-hackathons/02--react.mdx @@ -0,0 +1,174 @@ +--- +title: React +description: このチャプターでは、快適に XCode/VSCode で快適な C 言語を書くための秘訣を教えます。 +slug: textbook/web/for-hackathons/react + +--- + +import { Aside } from "@astrojs/starlight/components"; + +### Reactとは + +**React** は、Facebook(現Meta)によって開発された **JavaScriptライブラリ** で、UIを効率的に構築するために使われます。 + +#### Reactの特徴 + +1. **コンポーネントベース** + - UIを小さな **コンポーネント** に分けて作成できる。 + - それぞれのコンポーネントは独立しており、再利用が可能。 + + +2. **宣言的UI** + - UIの状態を直接操作するのではなく、データの変化に応じて自動で更新される。 + +3. **仮想DOM(Virtual DOM)** + - リアルDOMを直接操作するのではなく、**仮想的なDOM** を使って差分のみを更新するため、高速に動作する。 + + +4. **状態管理(State)** + - コンポーネントの状態(state)を管理し、動的なUIを実現できる。 + +5. **エコシステムが充実** + - `React Router`(ルーティング)や `Redux`(状態管理)など、多くのライブラリがある。 + +--- + +### 命令的 UI / 宣言的 UI + +以下のようなものを実装するとする。 + + +#### 1. 命令的 UI(Imperative UI) +従来の JavaScript や jQuery のように、手続き的に UI を変更する方法。 +**UIを更新する手順を細かく記述** しなければならず、コードが複雑になりやすい。 + +**例: jQueryを使った命令的UI** +```js +const button = document.getElementById("myButton"); +button.addEventListener("click", function () { + document.getElementById("message").innerText = "ボタンがクリックされました!"; +}); +``` +上記のコードでは、ボタンがクリックされるたびに `document.getElementById()` を使って、DOMを直接操作している。 + +#### 2. 宣言的 UI(Declarative UI) +Reactのように、**データの変化に応じてUIを自動更新する方法**。 +UIがどのように見えるべきかを **宣言的に記述** し、Reactが適切にレンダリングを行う。 + +**例: Reactを使った宣言的UI** +```jsx +import { useState } from 'react'; + +function App() { + const [message, setMessage] = useState(""); + + return ( +
+ +

{message}

+
+ ); +} + +export default App; +``` +このコードでは、ボタンがクリックされると `message` の状態が変化し、それに応じて自動的に `

{message}

` の内容が更新される。 + +**Reactのポイント:** +- `useState` を使って **状態(state)** を管理 +- UIの変更をReactに任せるため、**命令的なDOM操作が不要** + +**Reactの宣言的UIのメリット:** +- コードがシンプルで可読性が高い +- UIの更新を意識せずに済むため、バグが減る +- 状態の変化に応じたUIの自動更新が可能 + +--- + +### コンポーネントのインポート + +Reactの基本は **コンポーネント** であり、コンポーネントをインポートすることで再利用可能なUIを作成できる。 + +#### **1. コンポーネントの作成** +`MyComponent.js` という新しいファイルを作成し、Reactコンポーネントを定義する。 + +```jsx +function MyComponent() { + return

これは MyComponent です!

; +} + +export default MyComponent; +``` + +#### **2. コンポーネントのインポート** +`App.js` で `MyComponent` をインポートして使用する。 + +```jsx +import MyComponent from './MyComponent'; + +function App() { + return ( +
+ +
+ ); +} + +export default App; +``` + +#### **3. デフォルトエクスポートと名前付きエクスポート** + +**デフォルトエクスポート(default export)** +- 1つのファイルに1つのエクスポートを持つ。 +- インポート時に任意の名前を使える。 + +```jsx +// MyComponent.js +export default function MyComponent() { + return

デフォルトエクスポートのコンポーネント

; +} +``` +```jsx +// App.js +import MyComponent from './MyComponent'; +``` + +**名前付きエクスポート(named export)** +- 1つのファイルに複数のエクスポートが可能。 +- インポート時に **`{}` を使う必要がある**。 + +```jsx +// MyComponent.js +export function FirstComponent() { + return

最初のコンポーネント

; +} + +export function SecondComponent() { + return

2番目のコンポーネント

; +} +``` +```jsx +// App.js +import { FirstComponent, SecondComponent } from './MyComponent'; + +function App() { + return ( +
+ + +
+ ); +} + +export default App; +``` + +**デフォルトエクスポートと名前付きエクスポートの違い** +| 項目 | デフォルトエクスポート | 名前付きエクスポート | +|------|------------------|------------------| +| エクスポートの数 | 1つのファイルに1つだけ | 1つのファイルに複数可能 | +| インポート時の記述 | `import 任意の名前 from './ファイル名'` | `import { 名前 } from './ファイル名'` | +| インポート名の変更 | **可能** | **不可(指定した名前と一致する必要あり)** | \ No newline at end of file diff --git a/src/content/docs/textbook/web/for-hackathons/03--3moku.mdx b/src/content/docs/textbook/web/for-hackathons/03--3moku.mdx new file mode 100644 index 0000000..7fb9856 --- /dev/null +++ b/src/content/docs/textbook/web/for-hackathons/03--3moku.mdx @@ -0,0 +1,5077 @@ +--- +title: 実践 三目並べ +description: このチャプターでは、快適に XCode/VSCode で快適な C 言語を書くための秘訣を教えます。 +slug: textbook/web/for-hackathons/3moku + +--- + +import { Aside } from "@astrojs/starlight/components"; +import { LinkCard } from '@astrojs/starlight/components'; +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +### 1.完成形の確認 + +本記事では、Reactを使ってシンプルな三目並べゲームを作成します。 + +最終的に、以下のような機能を持つゲームを実装します。 + +- マス目のクリックで"X"と"O"を交互に配置 + +- 勝者が決まったら表示 + +- ゲームの履歴を保存し、過去の状態に戻れる機能 + +以下が完成形のコードです。 + +https://codesandbox.io/p/sandbox/react-dev-y4wfpq + + +{/* ファイル名がわかるといいな */} + +```jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
{status}
+
+ handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
+
+ handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
+
+ handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
+ + ); +} + +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 ( +
  • + +
  • + ); + }); + + return ( +
    +
    + +
    +
    +
      {moves}
    +
    +
    + ); +} + +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; +} +``` + +```css title="styles.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; +} +``` + +### 2.Reactプロジェクトのセットアップ +まずは、Reactプロジェクトをセットアップします。\ +以下のコマンドを実行して、`tic-tac-toe`という名前のReactプロジェクトを作成します。 + +```bash +npm create vite@latest tic-tac-toe +cd tic-tac-toe +npm run dev +``` +これで、Reactの開発サーバーが起動し、ブラウザでアプリケーションを確認できるようになります。 + + + +次に今回使用するcssファイルを書き換えます。\ +`src/App.css`を以下の内容を全て書き換えます。 + + +```css title="App.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; +} +``` + + + +```css title="App.css" +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + +``` + + + +### 3.Square コンポーネントの作成 +`Square`コンポーネントは、三目並べの盤面の1マスを表します。 \ +最初にシンプルなボタンとして作り、クリック時に動作するように設定しましょう。 + + + +```jsx title="App.jsx" +export default function Square() { + return ; +} +``` + + + +```diff lang="jsx" title="App.jsx" ++ export default function Square() { ++ return ; ++ } +``` + + + + +````diff lang="jsx" title="App.jsx" +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from '/vite.svg' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> +
    + + Vite logo + + + React logo + +
    +

    Vite + React

    +
    + +

    + Edit src/App.jsx and save to test HMR +

    +
    +

    + Click on the Vite and React logos to learn more +

    + + ) +} +export default App + +```` + +
    +
    + +次に複数個の盤面を表してみましょう。 + + +```jsx title="App.jsx" +// ... + <> + + + +// ... +``` + + +```diff lang="jsx" title="App.jsx" +export default function Square() { + return ( ++ <> ++ ++ ++ + ); +} +``` + + +````jsx title="App.jsx" + export default function Square() { + return ; + } +```` + + + +最後にマス目が9個になるまで何度かコピーアンドペーストしてみましょう。 + + +```jsx title="App.jsx" +// ... + +// ... +``` + + +```diff lang="jsx" title="App.jsx" +export default function Square() { + return ( + <> ++ ++ ++ ++ ++ ++ ++ ++ ++ + + ); +} +``` + + +````jsx title="App.jsx" +export default function Square() { + return ( + <> + + + + ); +} +```` + + + +これでは横に並んでしまいます。 + + +盤面のマス目はグリッド状に並べたいので以下のように変更しましょう。 +- `div` を使って複数のマス目を行単位でグループ化 +- CSS クラスを追加 + +ついでに、各マス目に番号をつけて、どれがどこに表示されているのか確認できるようにしましょう。 + + + +```jsx title="App.jsx" +// ... +
    + + + +
    +// ... +``` +
    + +```diff lang="jsx" title="App.jsx" ins="1" ins="2" ins="3" ins="4" ins="5" ins="6" ins="7" ins="8" ins="9" +export default function Square() { + return ( + <> ++
    + + + ++
    ++
    + + + ++
    ++
    + + + ++
    + + ); +} +``` +
    + +````jsx title="App.jsx" +export default function Square() { + return ( + <> + + + + + + + + + + + ); +} +```` + +
    + + +### 4.Board コンポーネントの作成 + +別の問題が出てきました。\ +`Square` という名前のコンポーネントなのに、実際にはもう 1 個のマス目ではなくなっています。\ +これを直すため、名前を Board に変えます。 + + + +```jsx title="App.jsx" +export default function Board() { +// ... +``` + + +```diff lang="jsx" title="App.jsx" ins="Board" +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` +
    + +````jsx title="App.jsx" +export default function Square() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +```` +
    +
    + +#### props を通してデータを渡す +次にユーザがマス目をクリックしたら、空白だった中身が"X"に変更するようにしたいと思います。 \ +しかし、現在のように盤面を作成していたのでは、この先マス目の中身を更新するコードを9回コピーアンドペーストしなくてはならなくなってしまいます。\ +そのように重複だらけのごちゃごちゃしたコードを書かずに済むように、再利用可能なコンポーネントを作成しましょう。 + + + +```jsx title="App.jsx" +function Square() { + return ; +} +// ... +``` + + +```diff lang="jsx" title="App.jsx" ++ function Square() { ++ return ; ++ } + +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` +
    + +````jsx title="App.jsx" +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +```` +
    +
    + +次に`Board`コンポーネント内で`Square`コンポーネントをレンダーするようにしましょう。 + + + +```jsx title="App.jsx" +// ... +export default function Board() { + return ( + <> +
    + + + +
    + // ... + <> + ) +} +``` +
    + +```diff lang="jsx" title="App.jsx" +function Square() { + return ; +} + +export default function Board() { + return ( + <> +
    ++ ++ ++ +
    +
    ++ ++ ++ +
    +
    ++ ++ ++ +
    + + ); +} +``` +
    + +````jsx title="App.jsx" +function Square() { + return ; +} + +export default function Board() { + // ... +} +```` + +
    + + + +現在の状況を見てみましょう。 + + +マス目が全て1になってしまいました。\ +これを修正するためには各マス目が、持つべき値を親コンポーネント(`Board`)から子コンポーネント(`Square`)に伝えるために、propsというものを使用します。 + +`Square`コンポーネントから、`Board`コンポーネントから`Value`プロパティを受け取れるようにします。 + + + +```jsx title="App.jsx" +function Square({ value }) { +// ... +``` + + +```diff lang="jsx" title="App.jsx" ins="{ value }" +function Square({ value }) { + return ; +} + +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` +
    + +````jsx title="App.jsx" +function Square() { + return ; +} + +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +```` +
    +
    + +`Value`プロパティを表示します。 \ +空白の盤面が表示されているはずです。 + + + + + +```jsx title="App.jsx" +function Square({ value }) { + return ; +} +// ... +``` + + +```diff lang="jsx" title="App.jsx" ins="{value}" +function Square({ value }) { + return ; +} + +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` +
    + +````jsx title="App.jsx" +function Square() { + return ; +} + +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +```` +
    +
    + + + +これはまだ、`Board`コンポーネントからpropsとして`value`を渡していないためです。 + + +```jsx title="App.jsx" +// ... +export default function Board() { + return ( + <> +
    + + + +
    + // ... + <> + ) +} +``` +
    + +```diff lang="jsx" title="App.jsx" ins="value="1"" ins="value="2"" ins="value="3"" ins="value="4"" ins="value="5"" ins="value="6"" ins="value="7"" ins="value="8"" ins="value="9" +function Square({ value }) { + return ; +} + +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} + +``` +
    + +````jsx title="App.jsx" +function Square({ value }) { + return ; +} + +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +```` +
    +
    + +もう一度、盤面を見てみましょう。 \ +以下のように表示されていれば完璧です。 + + + +### 5.インタラクティブなコンポーネントの作成 +`Square`コンポーネントをクリックすると`X`が表示されるようにしてみましょう。 + +`Square`の中に`handleClick`関数を宣言します。 + +以下のコードで`Square`コンポーネントをクリックすると、コンソールに`clicked!`が表示されているのがわかるはずです。 + + + + + +```jsx title="App.jsx" +function Square({ value }) { + function handleClick() { + console.log('clicked!'); + } + + return ( + + ); +} +// ... +``` + + +```diff lang="jsx" title="App.jsx" +function Square({ value }) { ++ function handleClick() { ++ console.log('clicked!'); ++ } + + return ( + + ); +} + +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` +
    + +````jsx title="App.jsx" +function Square({ value }) { + return ; +} + +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +```` +
    +
    + +次に`Square` コンポーネントに、クリックされたことを「記憶」して `X` マークを表示してもらうことにします。\ +何かを「記憶」するためには、`state` というものを使用します。 + +Reactでは、`useState`という特別な関数が用意されています。コンポーネントからこれを呼び出すことで「記憶」を行わせることができます。`Square` の現在の値を `state` に保存し、`Square` がクリックされたときにその値を変更しましょう。\ +App.jsxの先頭で`useState`をインポートします。 +次に`Square`コンポーネントの先頭に新しい行を追加してuseStateを宣言し、valueプロパティを削除します。 + + + +```jsx title="App.jsx" +import { useState } from 'react'; + +function Square() { + const [value, setValue] = useState(null); +// ... +``` + + +```diff lang="jsx" title="App.jsx" ++ import { useState } from 'react'; + +function Square() { ++ const [value, setValue] = useState(null); + function handleClick() { + console.log('clicked!'); + } + + return ( + + ); +} + +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value }) { + function handleClick() { + console.log('clicked!'); + } + + return ( + + ); +} + +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +```` +
    +
    + +`value`はstateの現在の値を格納し、`setValue`はその値を変更するために使う関数です。\ +` = useState(null)`ではstate変数の初期値としてnullを代入しています。 + + + +これで、`Square`コンポーネトでpropsを受け取らないようになったので、`Board`コンポーネント内の`Square`コンポーネントから`Value`プロパティを削除しましょう。 + + + + +```jsx title="App.jsx" +// ... +export default function Board() { + return ( + <> +
    + + + +
    + // ... + <> + ) +} +``` +
    + +```diff lang="jsx" title="App.jsx" +import { useState } from 'react'; + +function Square({ value }) { + function handleClick() { + console.log('clicked!'); + } + + return ( + + ); +} + +export default function Board() { + return ( + <> +
    ++ ++ ++ +
    +
    ++ ++ ++ +
    +
    ++ ++ ++ +
    + + ); +} + +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square() { + const [value, setValue] = useState(null); + function handleClick() { + console.log('clicked!'); + } + + return ( + + ); +} + +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} + +```` +
    +
    + +次にSquareをクリックすると"X"が表示されるようにします。\ +handleClick関数内の`console.log("clicked!");` を`setValue('X');`に置き換えます。\ +このsetValueが呼び出されることで、Reactに`Square`を再レンダーするように要求しています。 + + + +```jsx title="App.jsx" +// ... + function handleClick() { + setValue('X'); + } +// ... +``` + + +```diff lang="jsx" title="App.jsx" +import { useState } from 'react'; + +function Square() { + const [value, setValue] = useState(null); + + function handleClick() { ++ setValue('X'); + } + + return ( + + ); +} + +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value }) { + function handleClick() { + console.log('clicked!'); + } + + return ( + + ); +} + +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +```` +
    +
    + + +各 `Square` はそれぞれ独自の `state` を保持しています。\ +それぞれの `Square` に格納されている `value` は、他のものとは完全に独立しています。\ +コンポーネントの `set` 関数を呼び出すと、React は自動的に内部にある子コンポーネントも更新します。 + + +### 6.盤上の表示の実装 + +現在の状態とそこから見えてくる課題を認識しましょう。\ +現在は各 `Square` コンポーネントが、ゲームの状態を少しずつ保持している状況です。\ +しかし今後、勝ち負けの判定などを行うことを考えると、`Board` が 9 つの `Square` コンポーネントそれぞれの現在の state を、何らかの形で知る必要があります。\ +このような複数の子コンポーネントからデータを収集したい場合は、親コンポーネントに共有のstateを宣言するようにしてください。\ +親コンポーネントはそのstateを子コンポーネントにprops経由で渡すことができます。\ +これにより子同士及び親子間で、コンポーネントが同期されるようになります。 + + + + +```jsx title="App.jsx" +// ... +export default function Board() { + const [squares, setSquares] = useState(Array(9).fill(null)); +// ... +``` + + +```diff lang="jsx" title="App.jsx" +import { useState } from 'react'; + +function Square() { + const [value, setValue] = useState(null); + + function handleClick() { + setValue('X'); + } + + return ( + + ); +} + +export default function Board() { ++ const [squares, setSquares] = useState(Array(9).fill(null)); + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square() { + const [value, setValue] = useState(null); + + function handleClick() { + setValue('X'); + } + + return ( + + ); +} + +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +```` +
    +
    + +`Array(9).fill(null)` は、9個の要素を持つ配列を作成し、それぞれの要素を `null` で初期化します。\ +そしてその配列を `squares` として宣言します。 + +今後 `squares` 配列は盤面が埋まってくると以下のような見た目になる予定です。 +```jsx +['O', null, 'X', 'X', 'X', 'O', 'O', null, null] +``` + +そして作成した `Boar` コンポーネントは、レンダーする各 `Square` にpropsとして `value` を渡します。 + + + + +```jsx title="App.jsx" +// ... +export default function Board() { + const [squares, setSquares] = useState(Array(9).fill(null)); + return ( + <> +
    + + + +
    + // ... + <> + ) +} +``` +
    + +```diff lang="jsx" title="App.jsx" ins="value={squares[0]}" ins="value={squares[1]}" ins="value={squares[2]}" ins="value={squares[3]}" ins="value={squares[4]}" ins="value={squares[5]}" ins="value={squares[6]}" ins="value={squares[7]}" ins="value={squares[8]}" +import { useState } from 'react'; + +function Square() { + const [value, setValue] = useState(null); + + function handleClick() { + setValue('X'); + } + + return ( + + ); +} + +export default function Board() { + const [squares, setSquares] = useState(Array(9).fill(null)); + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square() { + const [value, setValue] = useState(null); + + function handleClick() { + setValue('X'); + } + + return ( + + ); +} + +export default function Board() { + const [squares, setSquares] = useState(Array(9).fill(null)); + return ( + // ... + ); +} +```` + +
    + +次に `Square` コンポーネントから、 `value` プロパティを受け取れるようにします。\ +この際に、 `value` は `Board` コンポーネントで管理しているため、 `handleClick` 関数と `value` をstateで管理していたコードを削除します。 + + + +```diff lang="jsx" title="App.jsx" ins="{ value }" +import { useState } from 'react'; + +function Square({value}) { + return ; +} + +export default function Board() { + const [squares, setSquares] = useState(Array(9).fill(null)); + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square() { + const [value, setValue] = useState(null); + + function handleClick() { + setValue('X'); + } + + return ( + + ); +} + +export default function Board() { + const [squares, setSquares] = useState(Array(9).fill(null)); + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +```` +
    +
    + +これで以下のように空白の盤面が表示されているはずです。 + + +しかしこれでは、 Square をクリックしても何も起きません。\ +そのため、 Square から Board のstateを更新する手段が必要です。\ +Board コンポーネントから Square コンポーネントにset関数を渡して、マス目がクリックされたときに Square にその関数を呼び出す形にします。 + + + +```diff lang="jsx" title="App.jsx" ins="onSquareClick" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +export default function Board() { + const [squares, setSquares] = useState(Array(9).fill(null)); + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({value}) { + return ; +} + +export default function Board() { + const [squares, setSquares] = useState(Array(9).fill(null)); + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +```` +
    +
    + +次にこの `onSquareClick` プロパティを、 `Board` コンポーネント内に `handleClick` という名前で作る関数に接続します。\ +`onSquareClick` を `handleClick` に接続するために、 `Square` コンポーネントの `onSquareClick` プロパティに関数を渡しましょう。\ +`onSquareClick={() => handleClick(0)}` の部分に関しては後で詳しく説明します。 + + + +```diff lang="jsx" title="App.jsx" ins="onSquareClick={() => handleClick(0)}" ins="onSquareClick={() => handleClick(1)}" ins="onSquareClick={() => handleClick(2)}" ins="onSquareClick={() => handleClick(3)}" ins="onSquareClick={() => handleClick(4)}" ins="onSquareClick={() => handleClick(5)}" ins="onSquareClick={() => handleClick(6)}" ins="onSquareClick={() => handleClick(7)}" ins="onSquareClick={() => handleClick(8)}" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +export default function Board() { + const [squares, setSquares] = useState(Array(9).fill(null)); + + return ( + <> +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +export default function Board() { + const [squares, setSquares] = useState(Array(9).fill(null)); + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +```` +
    +
    + +最後に盤面の状態を保持するstateである `squares` 配列を更新するため、 `Board` コンポーネント内に `handleClick` 関数を定義します。 +handleClick では、引数を受け取りその番目のマス目を"X"に更新しています。 + + + +```diff lang="jsx" title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +export default function Board() { + const [squares, setSquares] = useState(Array(9).fill(null)); + + return ( + <> +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} +```` +
    +
    + +これで改めて、盤面上の任意のマス目をクリックして"X"が置けるようになりました。 + + + + +### 7.手番の管理 + +まず先手がデフォルトで"X"になるようにしましょう。 \ +現在の手番を追跡するために、 `Board` コンポーネントにもう1つstateを追加します。 + + + +```diff lang="jsx" title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +export default function Board() { ++ const [xIsNext, setXIsNext] = useState(true); + const [squares, setSquares] = useState(Array(9).fill(null)); + + function handleClick(i) { + const nextSquares = squares.slice(); + nextSquares[i] = "X"; + setSquares(nextSquares); + } + + return ( + <> +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} +```` +
    +
    + +プレイヤーが着手するたびに、どちらのプレイヤーの手番なのかを決める`xIsNext`が反転して、ゲームの state が保存されます。\ +`Board` の `handleClick` 関数を書き換えて、そこで `xIsNext` の値を反転させましょう。 +これで、異なるマス目をクリックすると `X` と `O` が正しく交互に表示されるようになりました! + + + +```diff lang="jsx" title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +export default function Board() { + 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); + } + + return ( + <> +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +export default function Board() { + const [xIsNext, setXIsNext] = useState(true); + const [squares, setSquares] = useState(Array(9).fill(null)); + + function handleClick(i) { + const nextSquares = squares.slice(); + nextSquares[i] = "X"; + setSquares(nextSquares); + } + + return ( + <> +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} +```` +
    +
    + + + +しかし、これでは `X` が `O` で上書きされてしまっています! \ +そのため、選択したマス目に `X` や `O` の値があるかどうかチェックする必要があります。\ + + + + + +```diff lang="jsx" title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +export default function Board() { + const [xIsNext, setXIsNext] = useState(true); + const [squares, setSquares] = useState(Array(9).fill(null)); + + function handleClick(i) { ++ if (squares[i]) { ++ return; ++ } + const nextSquares = squares.slice(); + if (xIsNext) { + nextSquares[i] = "X"; + } else { + nextSquares[i] = "O"; + } + setSquares(nextSquares); + setXIsNext(!xIsNext); + } + + return ( + <> +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +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; +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +export default function Board() { + 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); + } + + return ( + <> +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} +```` +
    +
    + +### 8.勝敗の宣言 +次に勝敗が決まった時やこれ以上ゲームを進められなくなった際に、勝者の宣言を行うようにします。 \ +これを実現するために、9つのマス目の配列を受け取って勝者を判定し "X"、"O" またはnullを返す、 `calculateWinner` 関数を作成します。 + + + +```diff lang="jsx" title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +export default function Board() { + const [xIsNext, setXIsNext] = useState(true); + const [squares, setSquares] = useState(Array(9).fill(null)); + + function handleClick(i) { + if (squares[i]) { + return; + } + const nextSquares = squares.slice(); + if (xIsNext) { + nextSquares[i] = "X"; + } else { + nextSquares[i] = "O"; + } + setSquares(nextSquares); + setXIsNext(!xIsNext); + } + + return ( + <> +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + ++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; ++} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +export default function Board() { + const [xIsNext, setXIsNext] = useState(true); + const [squares, setSquares] = useState(Array(9).fill(null)); + + function handleClick(i) { + if (squares[i]) { + return; + } + const nextSquares = squares.slice(); + if (xIsNext) { + nextSquares[i] = "X"; + } else { + nextSquares[i] = "O"; + } + setSquares(nextSquares); + setXIsNext(!xIsNext); + } + + return ( + <> +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} +```` +
    +
    + +`Board` コンポーネントの `handleClick` 関数で `calculateWinner(squares)` を呼び出して、いずれかのプレーヤが勝利したかどうか判定します。 + + + +```diff lang="jsx" title="App.jsx" ins="squares[i] || calculateWinner(squares)" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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); + } + + return ( + <> +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +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; +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +export default function Board() { + const [xIsNext, setXIsNext] = useState(true); + const [squares, setSquares] = useState(Array(9).fill(null)); + + function handleClick(i) { + if (squares[i]) { + return; + } + const nextSquares = squares.slice(); + if (xIsNext) { + nextSquares[i] = "X"; + } else { + nextSquares[i] = "O"; + } + setSquares(nextSquares); + setXIsNext(!xIsNext); + + + } + + + + return ( + <> +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +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; +} +```` +
    +
    + +ゲームが終了したことを知らせるために、“Winner: X” または “Winner: O” というテキストを表示しましょう。\ +これを行うため、 `Board` コンポーネントに `status` 変数を追加します。この変数は、ゲームが終了した場合に勝者を表示し、ゲームが続行中の場合は、次がどちらの手番なのか表示します。 + + + +```jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +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; +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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); + } + + return ( + <> +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +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; +} +```` +
    +
    + +ここまでで三目並べの基本的な処理を行えます。 + +### 9.タイムトラベルの追加 + +ゲームを過去の手番に「巻き戻す」ことができる機能を追加しましょう。\ +過去の `squares` 配列を、`history` という名前の別の配列に入れて、それを新たにstateとして保持することにします。 + +```jsx +[ + // 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'], + // ... +] +``` + +新しいトップレベルのコンポーネント、 `Game` を作成して、過去の着手の一覧を表示するようにします。ゲームの履歴全体を保持するstateである `history` は、ここに置くことにします。\ +また、現在の手番(`xIsNext`)と着手の履歴(`history`)を管理するためのstateを追加します。 + + + +```diff lang="jsx" title="App.jsx" ins="const [history, setHistory] = useState([Array(9).fill(null)]);" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + + ++export default function Game() { ++ const [xIsNext, setXIsNext] = useState(true); ++ const [history, setHistory] = useState([Array(9).fill(null)]); ++ ++ return ( ++
    ++
    ++ ++
    ++
    ++
      {/*TODO*/}
    ++
    ++
    ++ ); ++} + +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; +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +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; +} +```` +
    +
    + +現在の盤面を表示するには、 `history` の最後にあるマス目の配列を読み取る必要があります。\ + + + +```diff lang="jsx" title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + + +export default function Game() { + const [xIsNext, setXIsNext] = useState(true); + const [history, setHistory] = useState([Array(9).fill(null)]); ++ const currentSquares = history[history.length - 1]; + + return ( +
    +
    + +
    +
    +
      {/*TODO*/}
    +
    +
    + ); +} + +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; +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + + +export default function Game() { + const [xIsNext, setXIsNext] = useState(true); + const [history, setHistory] = useState([Array(9).fill(null)]); + + return ( +
    +
    + +
    +
    +
      {/*TODO*/}
    +
    +
    + ); +} + +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; +} +```` +
    +
    + +次に、 `Game` コンポーネント内に、ゲーム内容を更新するための `Board` コンポーネントから呼ばれる `handlePlay` 関数を作成します。\ +`xIsNext` 、 `currentSquares` 、そして `handlePlay` を `Board` コンポーネントに props として渡すようにします。 + + + + +```diff lang="jsx" title="App.jsx" ins="xIsNext={xIsNext}" ins="currentSquares={currentSquares}" ins="handlePlay={handlePlay}" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +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) { ++ // TODO ++ } + + return ( +
    +
    + +
    +
    +
      {/*TODO*/}
    +
    +
    + ); +} + +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; +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + + +export default function Game() { + const [xIsNext, setXIsNext] = useState(true); + const [history, setHistory] = useState([Array(9).fill(null)]); + const currentSquares = history[history.length - 1]; + + return ( +
    +
    + +
    +
    +
      {/*TODO*/}
    +
    +
    + ); +} + +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; +} +```` +
    +
    + +次に、`Board` コンポーネントを編集し、props を受け取れるようにしましょう。 +また、ユーザーがマス目をクリックした際に `onPlay` 関数を発火させ、`Game` コンポーネントが `Board` を更新できるようにします。 + + + +```diff lang="jsx" title="App.jsx" ins="{ xIsNext, squares, onPlay }" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +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) { + // TODO + } + + return ( +
    +
    + +
    +
    +
      {/*TODO*/}
    +
    +
    + ); +} + +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; +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +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) { + // TODO + } + + return ( +
    +
    + +
    +
    +
      {/*TODO*/}
    +
    +
    + ); +} + +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; +} +```` +
    +
    + +Board コンポーネントは `Game` から渡される props によって制御されており、ゲームを動作させるには `handlePlay` 関数を実装する必要があります。\ +以前は `setSquares` で更新していましたが、現在は新しい `squares` 配列を `onPlay` に渡します。`handlePlay` では `history` を更新し、新しい `squares` を履歴に追加するとともに、`xIsNext` も切り替えましょう。 + + + + + +```diff lang="jsx" lang="jsx" title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +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); + } + + return ( +
    +
    + +
    +
    +
      {/*TODO*/}
    +
    +
    + ); +} + +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; +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +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) { + // TODO + } + + return ( +
    +
    + +
    +
    +
      {/*TODO*/}
    +
    +
    + ); +} + +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; +} +```` +
    +
    + +この時点で、state が `Game` コンポーネントに移動し終わり、前回と同様に完全に動作するようになっているはずです。 + +### 10.過去の着手の表示 + +三目並べのゲームの履歴が記録されるようになったので、プレーヤに過去の着手のリストを表示してみましょう。\ +` + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +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) { + // TODO + } + ++ const moves = history.map((squares, move) => { ++ let description; ++ if (move > 0) { ++ description = 'Go to move #' + move; ++ } else { ++ description = 'Go to game start'; ++ } ++ return ( ++
  • ++ ++
  • ++ ); ++ }); + + return ( +
    +
    + +
    +
    +
      {moves}
    +
    +
    + ); +} + +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; +} +``` + + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +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); + } + + return ( +
    +
    + +
    +
    +
      {/*TODO*/}
    +
    +
    + ); +} + +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; +} +```` +
    + + +### 11.keyを選ぶ + +リストをレンダーすると、React はレンダーされたリストの各アイテムに関するとある情報を保持します。そのリストが更新されると、React は何が変更されたのかを判断する必要があります。\ +`key` が指定されていない場合、React は警告を報告し、デフォルトでは配列のインデックスを `key` として使用します。\ +配列のインデックスを `key` として使用すると、リストの項目を並べ替えたり、挿入・削除したりする際に問題が生じます。\ +そのため、動的にリストを作成する際には、 `key` を割り当てることを強くお勧めします。\ +`key` はグローバルに一意である必要はなく、コンポーネントとその兄弟間で一意であれば十分です。 + + + +```diff lang="jsx" title="App.jsx" ins="key={move}" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +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) { + // TODO + } + + const moves = history.map((squares, move) => { + let description; + if (move > 0) { + description = 'Go to move #' + move; + } else { + description = 'Go to game start'; + } + return ( +
  • + +
  • + ); + }); + + return ( +
    +
    + +
    +
    +
      {moves}
    +
    +
    + ); +} + +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; +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +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) { + // TODO + } + + const moves = history.map((squares, move) => { + let description; + if (move > 0) { + description = 'Go to move #' + move; + } else { + description = 'Go to game start'; + } + return ( +
  • + +
  • + ); + }); + + return ( +
    +
    + +
    +
    +
      {moves}
    +
    +
    + ); +} + +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; +} +```` +
    +
    + +### 12.タイムトラベルの実装 + +`jumpTo` を実装する前に、 `Game` コンポーネントに、現在ユーザが見ているのが何番目の着手であるのかを管理させる必要があります。\ +これを行うために、 `currentMove` という名前の新しい state 変数を定義し、デフォルト値を 0 に設定します。\ +また、 `Game` 内の `jumpTo` 関数を更新して、 `currentMove` を更新するようにします。 `currentMove` を変更する数値が偶数の場合は、 `xIsNext` を `true` に設定します。 + + + +```diff lang="jsx" title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +export default function Game() { + const [xIsNext, setXIsNext] = useState(true); + const [history, setHistory] = useState([Array(9).fill(null)]); ++ const [currentMove, setCurrentMove] = useState(0); + const currentSquares = history[history.length - 1]; + + function handlePlay(nextSquares) { + setHistory([...history, nextSquares]); + setXIsNext(!xIsNext); + } + ++ function jumpTo(nextMove) { ++ setCurrentMove(nextMove); ++ setXIsNext(nextMove % 2 === 0); // 偶数の場合は、xIsNextをtrueに ++ } + + const moves = history.map((squares, move) => { + let description; + if (move > 0) { + description = 'Go to move #' + move; + } else { + description = 'Go to game start'; + } + return ( +
  • + +
  • + ); + }); + + return ( +
    +
    + +
    +
    +
      {moves}
    +
    +
    + ); +} + +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; +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +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) { + // TODO + } + + const moves = history.map((squares, move) => { + let description; + if (move > 0) { + description = 'Go to move #' + move; + } else { + description = 'Go to game start'; + } + return ( +
  • + +
  • + ); + }); + + return ( +
    +
    + +
    +
    +
      {moves}
    +
    +
    + ); +} + +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; +} +```` +
    +
    + +次に、 `Game` の `handlePlay` 関数を次の2点で修正しましょう。 +1. 過去に戻って新しい着手を行う場合、履歴の一部を保持し、それ以降を削除します。 `nextSquares` を `history` 全体ではなく、`history.slice(0, currentMove + 1)` の後に追加し、着手時点までの履歴のみを残すようにします。 +1. 着手のたびに `currentMove` を更新し、最新の履歴エントリを指すようにします。 + + + +```diff lang="jsx" title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +export default function Game() { + const [xIsNext, setXIsNext] = useState(true); + const [history, setHistory] = useState([Array(9).fill(null)]); + const [currentMove, setCurrentMove] = useState(0); + const currentSquares = history[history.length - 1]; + + function handlePlay(nextSquares) { + // 1.の変更 ++ const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]; ++ setHistory(nextHistory); + // 2.の変更 ++ setCurrentMove(nextHistory.length - 1); + setXIsNext(!xIsNext); + } + + function jumpTo(nextMove) { + setCurrentMove(nextMove); + setXIsNext(nextMove % 2 === 0); // 偶数の場合は、xIsNextをtrueに + } + + const moves = history.map((squares, move) => { + let description; + if (move > 0) { + description = 'Go to move #' + move; + } else { + description = 'Go to game start'; + } + return ( +
  • + +
  • + ); + }); + + return ( +
    +
    + +
    +
    +
      {moves}
    +
    +
    + ); +} + +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; +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +export default function Game() { + const [xIsNext, setXIsNext] = useState(true); + const [history, setHistory] = useState([Array(9).fill(null)]); + const [currentMove, setCurrentMove] = useState(0); + const currentSquares = history[history.length - 1]; + + function handlePlay(nextSquares) { + setHistory([...history, nextSquares]); + setXIsNext(!xIsNext); + } + + function jumpTo(nextMove) { + setCurrentMove(nextMove); + setXIsNext(nextMove % 2 === 0); // 偶数の場合は、xIsNextをtrueに + } + + const moves = history.map((squares, move) => { + let description; + if (move > 0) { + description = 'Go to move #' + move; + } else { + description = 'Go to game start'; + } + return ( +
  • + +
  • + ); + }); + + return ( +
    +
    + +
    +
    +
      {moves}
    +
    +
    + ); +} + +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; +} +```` +
    +
    + +最後に、 `Game` コンポーネントを変更し、常に最後の着手をレンダーする代わりに、現在選択されている着手をレンダーするようにします。 + + + +```diff lang="jsx" title="App.jsx" ins="history[currentMove]" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +export default function Game() { + const [xIsNext, setXIsNext] = useState(true); + const [history, setHistory] = useState([Array(9).fill(null)]); + const [currentMove, setCurrentMove] = useState(0); + const currentSquares = history[currentMove]; + + function handlePlay(nextSquares) { + // 1.の変更 + const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]; + setHistory(nextHistory); + // 2.の変更 + setCurrentMove(nextHistory.length - 1); + setXIsNext(!xIsNext); + } + + function jumpTo(nextMove) { + setCurrentMove(nextMove); + setXIsNext(nextMove % 2 === 0); // 偶数の場合は、xIsNextをtrueに + } + + const moves = history.map((squares, move) => { + let description; + if (move > 0) { + description = 'Go to move #' + move; + } else { + description = 'Go to game start'; + } + return ( +
  • + +
  • + ); + }); + + return ( +
    +
    + +
    +
    +
      {moves}
    +
    +
    + ); +} + +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; +} +``` +
    + +````jsx title="App.jsx" +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +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 ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +export default function Game() { + const [xIsNext, setXIsNext] = useState(true); + const [history, setHistory] = useState([Array(9).fill(null)]); + const [currentMove, setCurrentMove] = useState(0); + const currentSquares = history[history.length - 1]; + + function handlePlay(nextSquares) { + // 1.の変更 + const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]; + setHistory(nextHistory); + // 2.の変更 + setCurrentMove(nextHistory.length - 1); + setXIsNext(!xIsNext); + } + + function jumpTo(nextMove) { + setCurrentMove(nextMove); + setXIsNext(nextMove % 2 === 0); // 偶数の場合は、xIsNextをtrueに + } + + const moves = history.map((squares, move) => { + let description; + if (move > 0) { + description = 'Go to move #' + move; + } else { + description = 'Go to game start'; + } + return ( +
  • + +
  • + ); + }); + + return ( +
    +
    + +
    +
    +
      {moves}
    +
    +
    + ); +} + +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; +} +```` +
    +
    + +ゲーム履歴内にある任意の着手をクリックすると、三目並べの盤面が即座に更新され、その着手の発生後に対応する盤面が表示されるようになります。 + +