チュートリアル: 三目並べ
このチュートリアルでは、小さな三目並べゲームを作成します。このチュートリアルは、Reactの事前知識を一切必要としません。チュートリアルで学ぶ技術は、あらゆるReactアプリを構築するための基礎となるものであり、これを完全に理解することでReactに対する深い理解を得ることができます。
注意
このチュートリアルは、実践を通して学ぶことを好み、具体的なものを素早く作ってみたい人向けに設計されています。もし各概念を段階的に学ぶことを好む場合は、UIの記述
チュートリアルはいくつかのセクションに分かれています:
- チュートリアルの準備では、チュートリアルを進めるための開始点を提供します。
- 概要では、Reactの基礎(コンポーネント、props、state)を学びます。
- ゲームの完成では、React開発における最も一般的な技術を学びます。
- タイムトラベルの追加では、Reactの独自の強みに対するより深い洞察を得ることができます。
何を作るのか?
このチュートリアルでは、Reactを使ってインタラクティブな三目並べゲームを構築します。
完成したものがどのように見えるかは、こちらで確認できます:
もしコードがまだ理解できない場合や、コードの構文に慣れていない場合でも心配しないでください!このチュートリアルの目的は、Reactとその構文を理解する手助けをすることです。
チュートリアルを続ける前に、上記の三目並べゲームを確認することをお勧めします。気づく機能の一つは、ゲーム盤の右側に番号付きのリストがあることです。このリストはゲームで発生したすべての手の履歴を提供し、ゲームが進行するにつれて更新されます。
完成した三目並べゲームを試し終えたら、スクロールを続けてください。このチュートリアルでは、よりシンプルなテンプレートから始めます。次のステップは、ゲームの構築を開始できるように準備を整えることです。
チュートリアルの準備
以下のライブコードエディタで、右上隅のForkをクリックして、CodeSandboxウェブサイトを使用して新しいタブでエディタを開きます。CodeSandboxでは、ブラウザでコードを記述し、作成したアプリがユーザーにどのように見えるかをプレビューできます。新しいタブには空の四角形と、このチュートリアルのスターターコードが表示されるはずです。
注意
ローカルの開発環境を使用してこのチュートリアルを進めることもできます。そのためには、以下の手順が必要です:
- Node.jsをインストールする
- 先ほど開いたCodeSandboxのタブで、左上隅のボタンを押してメニューを開き、そのメニューからDownload Sandboxを選択してファイルのアーカイブをローカルにダウンロードする
- アーカイブを解凍し、ターミナルを開いて解凍したディレクトリに
cdする - 依存関係を
npm installでインストールする npm startを実行してローカルサーバーを起動し、プロンプトに従ってブラウザで実行中のコードを表示する
もし行き詰まっても、そこで止まらないでください!オンラインで進めて、後でローカル環境のセットアップを再度試してください。
概要
準備が整ったので、Reactの概要を見てみましょう!
スターターコードの確認
CodeSandboxでは、主に3つのセクションが表示されます:

- ファイルセクション。ここには、
App.js、index.js、styles.cssなどのファイルがsrcフォルダ内にリストされ、publicというフォルダもあります。 - コードエディタ。選択したファイルのソースコードが表示されます。
- ブラウザセクション。記述したコードがどのように表示されるかが確認できます。
App.jsファイルがファイルセクションで選択されているはずです。コードエディタに表示されるそのファイルの内容は次の通りです:
ブラウザセクションには、次のようなXが入った四角形が表示されているはずです:

それでは、スターターコードのファイルを見てみましょう。
App.js
App.js内のコードはコンポーネントを作成します。Reactでは、コンポーネントはユーザーインターフェースの一部を表す再利用可能なコードの断片です。コンポーネントは、アプリケーション内のUI要素をレンダリング、管理、更新するために使用されます。何が行われているのかを理解するために、コンポーネントを一行ずつ見ていきましょう:
最初の行はSquareという関数を定義しています。exportというJavaScriptキーワードは、この関数をこのファイルの外からアクセス可能にします。defaultキーワードは、あなたのコードを使用する他のファイルに対して、それがこのファイルのメイン関数であることを伝えます。
2行目はボタンを返します。returnというJavaScriptキーワードは、その後にあるものが関数の呼び出し元に値として返されることを意味します。<button>はJSX要素です。JSX要素は、表示したい内容を記述するJavaScriptコードとHTMLタグの組み合わせです。className="square"はボタンのプロパティ、つまりプロップであり、CSSにボタンのスタイルを指定する方法を伝えます。Xはボタン内に表示されるテキストであり、</button>はJSX要素を閉じ、以降のコンテンツがボタン内に配置されないことを示します。
styles.css
CodeSandboxのstyles.css最初の2つのファイルセクションにあるCSSセレクタ(*とbody)はアプリの大部分のスタイルを定義し、.squareセレクタは、classNameプロパティがsquareに設定されている任意のコンポーネントのスタイルを定義します。あなたのコードでは、これはApp.jsファイル内のSquareコンポーネントのボタンと一致します。
index.js
CodeSandboxのindex.jsというラベルのファイルをクリックしてくださいファイルセクションにあるApp.jsファイルで作成したコンポーネントとウェブブラウザとの橋渡し役です。
1〜5行目は、必要なすべての要素をまとめています:
- React
- ウェブブラウザと通信するためのReactのライブラリ(React DOM)
- コンポーネントのスタイル
- あなたが
App.jsで作成したコンポーネント
ファイルの残りの部分は、すべての要素をまとめ、最終的な成果物をpublicフォルダ内のindex.htmlに注入します。
ボードの構築
さて、App.jsに戻りましょう。ここがチュートリアルの残りの部分を過ごす場所です。
現在、ボードは単一のマス目にすぎませんが、9個必要です!もし単純にマス目をコピーして貼り付け、次のように2つのマス目を作ろうとすると:
次のエラーが発生します:
コンソール
/src/App.js: 隣接するJSX要素は囲みタグでラップする必要があります。JSXフラグメント<>...</>を使用する意図でしたか?
Reactコンポーネントは、2つのボタンのような複数の隣接するJSX要素ではなく、単一のJSX要素を返す必要があります。これを修正するには、フラグメント(<>と</>)を使用して、次のように複数の隣接するJSX要素をラップします:
これで次のように表示されるはずです:

素晴らしい!あとはコピー&ペーストを数回繰り返して9つのマス目を追加し…

おっと!マス目はすべて一列に並んでおり、ボードに必要なグリッド状になっていません。これを修正するには、マス目をdivで行ごとにグループ化し、いくつかのCSSクラスを追加する必要があります。ついでに、各マス目に番号を付けて、どこにどのマス目が表示されているか確認できるようにしましょう。
ファイルApp.jsで、Squareコンポーネントを次のように更新します:
で定義されたCSSは、styles.cssのclassNameがboard-rowのdivをスタイル付けします。スタイル付けされたdivでコンポーネントを行ごとにグループ化したので、三目並べボードが完成しました:

しかし、ここで問題が発生します。あなたのコンポーネントはSquareという名前ですが、もはや単一のマス目ではありません。名前をBoardに変更して修正しましょう:
この時点で、コードは次のようになっているはずです:
注記
シーッ…たくさん入力する必要がありますね!このページからコードをコピーして貼り付けても構いません。ただし、少し挑戦したい場合は、少なくとも一度は自分で手入力したコードのみをコピーすることをお勧めします。
propsを通じたデータの受け渡し
次に、ユーザーがマス目をクリックしたときに、マス目の値を空から「X」に変更したいと思います。これまでに構築したボードでは、マス目を更新するコードを9回(各マス目ごとに1回)コピー&ペーストする必要があります!コピー&ペーストする代わりに、Reactのコンポーネントアーキテクチャを使用して再利用可能なコンポーネントを作成し、煩雑で重複したコードを避けることができます。
まず、<button className="square">1</button>コンポーネントから最初のマス目を定義する行(Board)をコピーして、新しいSquareコンポーネントに貼り付けます:
次に、Boardコンポーネントを更新して、JSX構文を使用してそのSquareコンポーネントをレンダリングします:
ブラウザのdivとは異なり、独自のコンポーネントであるBoardとSquareは大文字で始める必要があることに注意してください。
見てみましょう:

大変!以前あった番号付きのマスが失われてしまいました。今では各マスが「1」と表示されています。これを修正するには、親コンポーネント(Board)から子コンポーネント(Square)へ、各マスが持つべき値をを使って渡します。
Boardから渡すSquareコンポーネントを更新します:valuepropを読み取るように
function Square({ value })は、Squareコンポーネントがvalueというpropを受け取れることを示しています。
次に、各マス内でvalueを表示したいと思います。次のようにしてみてください:1の代わりにその
おっと、これは望んでいたものではありません:

コンポーネント内のJavaScript変数であるvalueをレンダリングしたいのであって、「value」という単語ではありません。JSXからJavaScriptに「エスケープ」するには、中括弧が必要です。JSX内のvalueの周りに中括弧を追加します:
今のところ、空のボードが表示されるはずです:

これは、Boardコンポーネントが、レンダリングする各valuepropをまだ渡していないためですSquareコンポーネントにvaluepropを追加します:Squareコンポーネントに
これで、再び数字のグリッドが表示されるはずです:

更新されたコードは次のようになります:
インタラクティブなコンポーネントを作成する
クリックしたときにSquareコンポーネントにXを埋めましょう。Squareコンポーネント内に関数handleClickを宣言しますonClickを追加します:
今、四角をクリックすると、CodeSandboxの"clicked!"というログが表示されるはずですブラウザセクションの下部にあるコンソールタブに"clicked!"がログされます。同じメッセージのコンソールログが繰り返されても、コンソールに新しい行は作成されません。代わりに、最初の"clicked!"ログの横に増加するカウンターが表示されます。
注意
このチュートリアルをローカルの開発環境で進めている場合は、ブラウザのコンソールを開く必要があります。例えば、Chromeブラウザを使用している場合、キーボードショートカットShift + Ctrl + J(Windows/Linux)またはOption + ⌘ + J(macOS)でコンソールを表示できます。
次のステップとして、Squareコンポーネントがクリックされたことを「記憶」し、「X」マークで埋めるようにしたいと思います。何かを「記憶」するために、コンポーネントは状態を使用します。
Reactは、コンポーネントから呼び出して「記憶」させるための特別な関数useStateを提供しています。Squareの現在の値を状態に保存し、Squareがクリックされたときに変更しましょう。
ファイルの先頭でuseStateをインポートします。Squareコンポーネントからvalueプロップを削除します。代わりに、Squareの先頭に新しい行を追加してuseStateを呼び出します。それをvalueという状態変数を返すようにします:
valueは値を保存し、setValueは値を変更するために使用できる関数です。useStateに渡されたnullはこの状態変数の初期値として使用されるため、ここでのvalueは最初nullと等しくなります。
これでSquareコンポーネントはプロップを受け取らなくなったので、Boardコンポーネントによって作成される9つのSquareコンポーネントすべてからvalueプロップを削除します:
次に、クリックされたときに「X」を表示するようにSquareを変更します。イベントハンドラーのconsole.log("clicked!");をsetValue('X');に置き換えます。これでSquareコンポーネントは次のようになります:
このset関数をonClickハンドラーから呼び出すことで、そのSquareの<button>がクリックされるたびにReactに再レンダリングするように指示しています。更新後、Squareのvalueは'X'になるので、ゲームボード上に「X」が表示されます。任意のSquareをクリックすると、「X」が表示されるはずです:

各Squareは独自の状態を持っています:各Squareに保存されているvalueは他のSquareとは完全に独立しています。コンポーネント内でset関数を呼び出すと、Reactは内部の子コンポーネントも自動的に更新します。
上記の変更を行った後、コードは次のようになります:
React Developer Tools
React DevTools を使用すると、React コンポーネントの props と state を確認できます。React DevTools のタブは、CodeSandbox のブラウザセクションの下部にあります:

画面上の特定のコンポーネントを検査するには、React DevTools の左上隅にあるボタンを使用します:

ゲームを完成させる
ここまでで、三目並べゲームの基本的な構成要素はすべて揃いました。ゲームを完成させるには、盤上に「X」と「O」を交互に配置する方法と、勝者を判定する方法が必要です。
状態のリフトアップ
現在、各 Squareコンポーネントがゲームの状態の一部を保持しています。三目並べゲームで勝者を確認するには、Boardコンポーネントが、9つの Squareコンポーネントそれぞれの状態を何らかの方法で知る必要があります。
どのようにアプローチすればよいでしょうか?最初は、Boardが各 SquareにそのSquareの状態を「尋ねる」必要があると推測するかもしれません。このアプローチは技術的には React で可能ですが、コードが理解しにくくなり、バグの影響を受けやすく、リファクタリングが困難になるため、推奨されません。代わりに、最善の方法は、ゲームの状態を各Squareではなく、親の Boardコンポーネントに保存することです。Boardコンポーネントは、各Squareに何を表示するかを prop を渡して指示できます。これは、各 Square に数字を渡したときと同じです。
複数の子からデータを収集したり、2つの子コンポーネントが互いに通信したりするには、共有状態を親コンポーネントで宣言します。親コンポーネントは、その状態を props を介して子に渡し戻すことができます。これにより、子コンポーネント同士、および親コンポーネントとの同期が保たれます。
状態を親コンポーネントにリフトアップすることは、React コンポーネントをリファクタリングする際によく行われます。
この機会に試してみましょう。Boardコンポーネントを編集して、squaresという名前の状態変数を宣言します。デフォルト値は、9つのマスに対応する9つの null の配列です:
Array(9).fill(null)は、9つの要素を持つ配列を作成し、それぞれをnullに設定します。その周りのuseState()呼び出しは、最初にその配列に設定されるsquares状態変数を宣言します。配列の各エントリは、マスの値に対応します。後で盤を埋めると、squares配列は次のようになります:
次に、Boardコンポーネントは、valueprop を、レンダリングする各Squareに渡す必要があります:
次に、Squareコンポーネントを編集して、Board コンポーネントからvalueprop を受け取るようにします。これには、Square コンポーネント自身によるvalueの状態管理と、ボタンのonClickprop を削除する必要があります:
この時点で、空の三目並べボードが表示されるはずです:

そして、コードは次のようになっているはずです:
各Squareコンポーネントは、valueというプロップを受け取るようになりました。この値は、'X'、'O'、または空のマスを示すnullのいずれかになります。
次に、Squareがクリックされたときに何が起こるかを変更する必要があります。Boardコンポーネントは、どのマスが埋まっているかを管理するようになりました。SquareがBoardの状態を更新する方法を作成する必要があります。状態はそれを定義するコンポーネントにプライベートであるため、Squareから直接Boardの状態を更新することはできません。
代わりに、BoardコンポーネントからSquareコンポーネントに関数を渡し、マスがクリックされたときにSquareがその関数を呼び出すようにします。Squareコンポーネントがクリックされたときに呼び出す関数から始めます。この関数をonSquareClickと呼びます:
次に、onSquareClick関数をSquareコンポーネントのプロップに追加します:
次に、onSquareClickプロップをBoardコンポーネント内のhandleClickという名前の関数に接続します。onSquareClickをhandleClickに接続するには、最初のSquareコンポーネントのonSquareClickプロップに関数を渡します:
最後に、ボードの状態を保持するsquares配列を更新するために、Boardコンポーネント内にhandleClick関数を定義します:
このhandleClick関数は、JavaScriptのslice()配列メソッドを使ってsquares配列のコピー(nextSquares)を作成します。そして、handleClickはnextSquares配列を更新して、最初のマス(インデックス[0])にXを追加します。
このsetSquares関数を呼び出すと、Reactはコンポーネントの状態が変化したことを認識します。これにより、squares状態を使用するコンポーネント(Board)とその子コンポーネント(ボードを構成するSquareコンポーネント)の再レンダリングがトリガーされます。
注記
JavaScriptはクロージャをサポートしています。これは、内側の関数(例:handleClick)が外側の関数(例:Board)で定義された変数や関数にアクセスできることを意味します。handleClick関数は、squares状態を読み取り、setSquaresメソッドを呼び出すことができます。なぜなら、これらは両方ともBoard関数の内側で定義されているからです。
これで、ボードにXを追加できるようになりました… ただし、左上のマスにのみです。あなたのhandleClick関数は、左上のマスのインデックス(0)を更新するようにハードコードされています。handleClickを更新して、任意のマスを更新できるようにしましょう。handleClick関数に、更新するマスのインデックスを受け取る引数iを追加します:
次に、そのiをhandleClickに渡す必要があります。JSX内で直接、squareのonSquareClickプロップをhandleClick(0)に設定しようとするかもしれませんが、これは機能しません:
これが機能しない理由は次の通りです。handleClick(0)の呼び出しは、ボードコンポーネントのレンダリングの一部となります。handleClick(0)はsetSquaresを呼び出してボードコンポーネントの状態を変更するため、ボードコンポーネント全体が再レンダリングされます。しかし、これにより再びhandleClick(0)が実行され、無限ループが発生します:
コンソール
再レンダリングが多すぎます。Reactは無限ループを防ぐためにレンダリング回数を制限しています。
なぜこの問題は以前発生しなかったのでしょうか?
以前onSquareClick={handleClick}を渡していたときは、handleClick関数をプロップとして渡していました。呼び出してはいなかったのです!しかし今は、その関数をすぐに呼び出しています—handleClick(0)の括弧に注目してください—これが早すぎる実行の原因です。ユーザーがクリックするまではhandleClickを呼び出したくないのです!
これは、handleFirstSquareClickのようにhandleClick(0)を呼び出す関数や、handleSecondSquareClickのようにhandleClick(1)を呼び出す関数などを作成することで修正できます。これらの関数をonSquareClick={handleFirstSquareClick}のように(呼び出すのではなく)プロップとして渡します。これで無限ループは解決します。
しかし、9つの異なる関数を定義し、それぞれに名前を付けるのは冗長すぎます。代わりに、次のようにしましょう:
新しい() =>構文に注目してください。ここで、() => handleClick(0)はアロー関数であり、関数を定義する短い方法です。squareがクリックされると、=>「矢印」の後のコードが実行され、handleClick(0)が呼び出されます。
次に、他の8つのsquareを更新して、渡したアロー関数からhandleClickを呼び出すようにします。handleClickの各呼び出しの引数が、正しいsquareのインデックスに対応していることを確認してください:
これで、ボード上の任意のsquareをクリックしてXを追加できるようになりました:

しかし今回は、すべての状態管理がBoardコンポーネントによって処理されています!
コードは次のようになるはずです:
状態管理がBoardコンポーネント内にあるため、親であるBoardコンポーネントは、子であるSquareコンポーネントにプロップを渡し、正しく表示できるようにします。Squareをクリックすると、子であるSquareコンポーネントは、親であるBoardコンポーネントにボードの状態を更新するよう要求します。Boardの状態が変化すると、Boardコンポーネントとすべての子Squareが自動的に再レンダリングされます。すべてのsquareの状態をBoardコンポーネント内に保持することで、将来的に勝者を判定できるようになります。
ユーザーがボードの左上のsquareをクリックしてXを追加するときに何が起こるか、まとめてみましょう:
- 左上のマスをクリックすると、
buttonがSquareからonClickプロップとして受け取った関数が実行されます。Squareコンポーネントはその関数をBoardからonSquareClickプロップとして受け取りました。Boardコンポーネントはその関数をJSX内で直接定義しています。それは引数0を指定してhandleClickを呼び出します。 handleClickは引数(0)を使用して、squares配列の最初の要素をnullからXに更新します。Boardコンポーネントのsquares状態が更新されたため、Boardとそのすべての子コンポーネントが再レンダリングされます。これにより、インデックス0のSquareコンポーネントのvalueプロップがnullからXに変更されます。
最終的に、ユーザーは左上のマスがクリック後に空からXを持つように変化したことを確認します。
注記
DOMの<button>要素のonClick属性は、組み込みコンポーネントであるため、Reactにとって特別な意味を持ちます。Squareのようなカスタムコンポーネントでは、命名は開発者に委ねられます。SquareのonSquareClickプロップやBoardのhandleClick関数には任意の名前を付けることができ、コードは同じように動作します。Reactでは、イベントを表すプロップにはonSomethingという名前を、それらのイベントを処理する関数定義にはhandleSomethingという名前を使用するのが慣例です。
不変性が重要な理由
handleClickでは、既存の配列を変更する代わりに.slice()を呼び出してsquares配列のコピーを作成していることに注目してください。その理由を説明するために、不変性と、なぜ不変性を学ぶことが重要なのかについて議論する必要があります。
データを変更するには、一般的に2つのアプローチがあります。1つ目のアプローチは、データの値を直接変更することでデータをミューテート(変更)する方法です。2つ目のアプローチは、データを、目的の変更を加えた新しいコピーで置き換える方法です。squares配列をミューテートした場合の例を以下に示します:
そして、squares配列をミューテートせずにデータを変更した場合の例を以下に示します:
結果は同じですが、直接ミューテート(基盤となるデータを変更)しないことで、いくつかの利点が得られます。
不変性により、複雑な機能の実装がはるかに簡単になります。このチュートリアルの後半では、ゲームの履歴を確認し、過去の手に「戻る」ことができる「タイムトラベル」機能を実装します。この機能はゲームに特有のものではなく、特定のアクションを元に戻したりやり直したりする機能は、アプリケーションで一般的に求められる要件です。直接的なデータのミューテートを避けることで、データの以前のバージョンをそのまま保持し、後で再利用することができます。
不変性にはもう1つの利点があります。デフォルトでは、親コンポーネントの状態が変化すると、すべての子コンポーネントが自動的に再レンダリングされます。これには、変更の影響を受けなかった子コンポーネントも含まれます。再レンダリング自体はユーザーにとって気づかれないものですが(積極的に避けようとするべきではありません!)、パフォーマンス上の理由から、明らかに影響を受けていないツリーの一部の再レンダリングをスキップしたい場合があるかもしれません。不変性により、コンポーネントが自身のデータが変更されたかどうかを比較するコストが非常に低くなります。Reactがいつコンポーネントを再レンダリングするかを選択する方法については、memo APIリファレンスで詳しく学ぶことができます。
手番の交代
さて、この三目並べゲームの大きな欠陥を修正する時が来ました:盤面に「O」を記録することができません。
最初の手をデフォルトで「X」に設定します。Boardコンポーネントにもう1つの状態を追加して、これを追跡しましょう:
プレイヤーが移動するたびに、xIsNext(ブール値)が反転して次のプレイヤーが決定され、ゲームの状態が保存されます。BoardのhandleClick関数を更新して、xIsNextの値を反転させます:
これで、異なるマス目をクリックすると、XとOが交互に表示されるようになります!
しかし、問題があります。同じマス目を複数回クリックしてみてください:

このXはOによって上書きされてしまいます!これはゲームに非常に興味深い変化をもたらすかもしれませんが、今は元のルールに従いましょう。
マス目にXまたはOを記入する際、そのマス目に既にXまたはOの値があるかどうかを最初に確認していません。これは早期リターンによって修正できます。マス目に既にXまたはOがあるかどうかを確認します。マス目が既に埋まっている場合、handleClick関数内で、ボードの状態を更新しようとする前にreturnします。
これで、空のマス目にのみXまたはOを追加できるようになりました!この時点でのコードは以下のようになります:
勝者の宣言
プレイヤーが交互に手を打てるようになったので、ゲームが勝利したときやこれ以上手を打てないときに表示したいと思います。そのために、9つのマス目の配列を受け取り、勝者をチェックして適切にcalculateWinnerというヘルパー関数を追加します。'X'、'O'、またはnullを返すcalculateWinner関数についてはあまり気にしないでください。これはReactに特有のものではありません:
注記
このcalculateWinnerをBoardの前後に定義しても問題ありません。コンポーネントを編集するたびにスクロールする必要がないように、最後に置きましょう。
プレイヤーが勝利したかどうかを確認するために、BoardコンポーネントのhandleClick関数内でcalculateWinner(squares)を呼び出します。このチェックは、ユーザーが既にXまたはOがあるマス目をクリックしたかどうかを確認するのと同時に行えます。どちらの場合も早期リターンしたいと思います:
ゲームが終了したことをプレイヤーに知らせるには、「勝者: X」や「勝者: O」といったテキストを表示します。そのために、statusセクションをBoardコンポーネントに追加します。statusは、ゲームが終了していれば勝者を表示し、ゲームが進行中であれば次にどちらのプレイヤーのターンかを表示します:
おめでとうございます!これで動作する三目並べゲームが完成しました。そして、Reactの基本も学びました。ですから、あなたこそが真の勝者です。コードは以下のようになります:
タイムトラベルの追加
最後の演習として、ゲームの過去の手に「時間を遡って」戻れるようにしましょう。
手の履歴の保存
もしsquares配列をミューテートしていたら、タイムトラベルの実装は非常に難しくなります。
しかし、あなたはslice()を使って、各手の後にsquares配列の新しいコピーを作成し、それをイミュータブルとして扱いました。これにより、過去のsquares配列のすべてのバージョンを保存し、既に行われたターン間を移動することが可能になります。
過去のsquares配列を、historyという別の配列に保存します。これは新しい状態変数として保存します。history配列は、最初の手から最後の手までのすべての盤面の状態を表し、以下のような形になります:
状態のリフトアップ、再び
これから、過去の手の一覧を表示するために、Gameという新しいトップレベルコンポーネントを作成します。そこに、ゲーム全体の履歴を含むhistory状態を配置します。
状態historyをGameコンポーネントに配置することで、その子であるBoardコンポーネントからsquares状態を削除できます。SquareコンポーネントからBoardコンポーネントへ「状態をリフトアップ」したのと同様に、今度はBoardからトップレベルのGameコンポーネントへリフトアップします。これにより、GameコンポーネントがBoardのデータを完全に制御し、Boardにhistoryから過去のターンをレンダリングするよう指示できるようになります。
まず、Gameコンポーネントをexport default付きで追加します。Boardコンポーネントといくつかのマークアップをレンダリングするようにします:
ここでは、export default キーワードを function Board() { 宣言の前から削除し、function Game() {宣言の前に追加していることに注意してください。これにより、index.jsファイルは、Board コンポーネントではなく Gameコンポーネントを最上位のコンポーネントとして使用するようになります。Gameコンポーネントが返す追加のdivは、後でボードに追加するゲーム情報のためのスペースを確保しています。
次にどのプレイヤーの手番か、および手の履歴を追跡するために、Game コンポーネントに状態を追加します:
[Array(9).fill(null)]は、単一のアイテムを持つ配列であり、そのアイテム自体が9つのnullの配列であることに注意してください。
現在の手のマス目をレンダリングするには、historyから最後のマス目配列を読み取る必要があります。これにはuseStateは必要ありません。レンダリング中に計算するのに十分な情報が既にあります:
次に、ゲームを更新するためにBoardコンポーネントから呼び出されるhandlePlay 関数を Gameコンポーネント内に作成します。xIsNext、currentSquares、およびhandlePlayを props としてBoardコンポーネントに渡します:
Boardコンポーネントが受け取る props によって完全に制御されるようにしましょう。Boardコンポーネントが3つの props を受け取るように変更します:xIsNext、squares、そして新しいonPlay 関数です。この関数は、プレイヤーが手を打ったときに更新されたマス目配列を渡して Board が呼び出すことができます。次に、Board関数の最初の2行(useStateを呼び出している部分)を削除します:
次に、Boardコンポーネント内のhandleClick 内の setSquares と setXIsNext の呼び出しを、新しい onPlay関数への単一の呼び出しに置き換えますGameコンポーネントがBoardを更新できるようになります:
Board コンポーネントは、Gameコンポーネントから渡される props によって完全に制御されるようになりました。ゲームを再び動作させるには、Gameコンポーネント内のhandlePlay関数を実装する必要があります。
handlePlayは呼び出されたときに何をすべきでしょうか?Board は以前、更新された配列で setSquaresを呼び出していましたが、今は更新されたsquares配列をonPlay に渡していることを思い出してください。
handlePlay関数は、再レンダリングをトリガーするためにGameの状態を更新する必要がありますが、もはや呼び出せるsetSquares関数はありません。この情報を保存するためにhistory 状態変数を使用しています。historyを更新するには、更新されたsquares配列を新しい履歴エントリとして追加します。また、以前Boardが行っていたようにxIsNext を切り替える必要があります:
ここで、[...history, nextSquares]は、history内のすべての項目と、その後に続くnextSquaresを含む新しい配列を作成します。(...historyスプレッド構文を「history)
例えば、historyが[[null,null,null], ["X",null,null]]で、nextSquaresが["X",null,"O"]の場合、新しい[...history, nextSquares]配列は[[null,null,null], ["X",null,null], ["X",null,"O"]]になります。
この時点で、状態はGameコンポーネント内に存在するようになり、UIはリファクタリング前と同様に完全に機能するはずです。この時点でのコードは以下のようになります:
過去の手を表示する
三目並べゲームの履歴を記録しているので、プレイヤーに過去の手のリストを表示できるようになりました。
React要素(<button>など)は通常のJavaScriptオブジェクトであり、アプリケーション内で受け渡しできます。Reactで複数のアイテムをレンダリングするには、React要素の配列を使用できます。
状態には既にhistoryの手の配列があるので、これをReact要素の配列に変換する必要があります。JavaScriptでは、ある配列を別の配列に変換するために、配列のmapメソッドを使用できます:
あなたはmapを使用して、historyの手を画面上のボタンを表すReact要素に変換し、過去の手に「ジャンプ」するためのボタンのリストを表示します。Gameコンポーネント内でhistoryに対してmapを実行しましょう:
以下にコードがどのようになるべきか確認できます。開発者ツールのコンソールに次のようなエラーが表示されることに注意してください:
このエラーは次のセクションで修正します。
mapに渡した関数内でhistory配列を反復処理する際、squares引数はhistoryの各要素を、move引数は各配列インデックス(0、1、2)
三目並べゲームの履歴の各手に対して、ボタン<button>を含むリスト項目<li>を作成します。このボタンには、jumpToという関数(まだ実装していません)を呼び出すonClickハンドラーがあります。
現時点では、ゲームで発生した手のリストと、開発者ツールコンソールにエラーが表示されるはずです。「key」エラーの意味について説明しましょう。
キーの選択
リストをレンダリングする際、Reactはレンダリングされた各リスト項目について情報を保存します。リストを更新するとき、Reactは何が変更されたかを判断する必要があります。リスト項目が追加、削除、並べ替え、または更新された可能性があります。
次のような遷移を想像してください:
から
更新されたカウントに加えて、人間がこれを読めば、AlexaとBenの順序を入れ替え、AlexaとBenの間にClaudiaを挿入したと考えるでしょう。しかし、Reactはコンピュータープログラムであり、あなたの意図を知らないため、各リスト項目を兄弟要素と区別するために、各リスト項目にkeyプロパティを指定する必要があります。データがデータベースからのものであれば、Alexa、Ben、ClaudiaのデータベースIDをキーとして使用できます。
リストが再レンダリングされるとき、Reactは各リスト項目のキーを取り、前のリストの項目から一致するキーを検索します。現在のリストに以前存在しなかったキーがある場合、Reactはコンポーネントを作成します。現在のリストに以前存在したキーが欠けている場合、Reactは以前のコンポーネントを破棄します。2つのキーが一致する場合、対応するコンポーネントが移動されます。
キーは各コンポーネントの識別子をReactに伝え、Reactが再レンダリング間で状態を維持できるようにします。コンポーネントのキーが変更されると、コンポーネントは破棄され、新しい状態で再作成されます。
keyはReactにおける特別で予約されたプロパティです。要素が作成されるとき、Reactはkeyプロパティを抽出し、キーを返された要素に直接格納します。keyはpropsとして渡されているように見えるかもしれませんが、Reactは自動的にkeyを使用して更新するコンポーネントを決定します。コンポーネントが親が指定したkeyを問い合わせる方法はありません。
動的なリストを構築する際は、適切なキーを割り当てることを強くお勧めします。適切なキーがない場合は、データ構造を再検討してキーを持つようにすることを検討してください。
キーが指定されていない場合、Reactはエラーを報告し、デフォルトで配列インデックスをキーとして使用します。配列インデックスをキーとして使用すると、リスト項目の並べ替えや挿入/削除を行う際に問題が発生します。key={i}を明示的に渡すとエラーは表示されなくなりますが、配列インデックスと同じ問題があり、ほとんどの場合推奨されません。
キーはグローバルに一意である必要はなく、コンポーネントとその兄弟要素間で一意であれば十分です。
タイムトラベルの実装
三目並べゲームの履歴では、過去の各手に一意のIDが関連付けられています:それは手の連番です。手が並べ替えられたり、削除されたり、途中に挿入されたりすることはないため、手のインデックスをキーとして使用しても安全です。
Game関数では、キーを<li key={move}>として追加できます。レンダリングされたゲームをリロードすると、Reactの「key」エラーは消えるはずです:
これを行うには、currentMoveという新しい状態変数を定義し、デフォルトを0に設定しますjumpToこれを行うには、currentMoveという新しい状態変数を定義し、デフォルトを0に設定しますGameを実装する前に、Gameコンポーネントがユーザーが現在どの手順を表示しているかを追跡する必要があります。これを行うには、currentMoveという新しい状態変数を定義し、デフォルトを0に設定します。currentMoveこれを行うには、currentMoveという新しい状態変数を定義し、デフォルトを0に設定します0これを行うには、currentMoveという新しい状態変数を定義し、デフォルトを0に設定します
次に、Game内のjumpTo関数を更新して、currentMoveを更新しますjumpTo次に、Game内のjumpTo関数を更新して、currentMoveを更新しますGame次に、Game内のjumpTo関数を更新して、currentMoveを更新しますcurrentMove次に、Game内のjumpTo関数を更新して、currentMoveを更新しますxIsNext次に、Game内のjumpTo関数を更新して、currentMoveを更新しますtrue次に、Game内のjumpTo関数を更新して、currentMoveを更新します。また、currentMoveを変更する数が偶数の場合、xIsNextをtrueに設定します。currentMove次に、Game内のjumpTo関数を更新して、currentMoveを更新します
次に、マス目をクリックしたときに呼び出されるGameのhandlePlay関数に2つの変更を加えます。Game次に、マス目をクリックしたときに呼び出されるGameのhandlePlay関数に2つの変更を加えます。handlePlay次に、マス目をクリックしたときに呼び出されるGameのhandlePlay関数に2つの変更を加えます。
- 「過去に戻って」その時点から新しい手を打つ場合、その時点までの履歴のみを保持したいとします。history内のすべての項目(...スプレッド構文)の後にnextSquaresを追加するのではなく、history.slice(0, currentMove + 1)内のすべての項目の後に追加して、古い履歴のその部分のみを保持するようにします。
nextSquareshistory内のすべての項目(...history内のすべての項目(historyスプレッド構文)の後にnextSquaresを追加するのではなく、historyhistory.slice(0, currentMove + 1)slice(0, currentMove + 1)内のすべての項目の後に追加して、古い履歴のその部分のみを保持するようにします - 手が打たれるたびに、currentMoveを最新の履歴エントリを指すように更新する必要があります。
currentMove手が打たれるたびに、currentMoveを最新の履歴エントリを指すように更新する必要があります。
最後に、Gameコンポーネントを変更して、常に最終手をレンダリングするのではなく、現在選択されている手をレンダリングするようにします。Game最後に、Gameコンポーネントを変更して、常に最終手をレンダリングするのではなく、現在選択されている手をレンダリングするようにします。
ゲームの履歴の任意の手順をクリックすると、三目並べのボードがその手順の後の状態に即座に更新されるはずです。
最終的な整理
コードをよく見ると、xIsNext === trueはcurrentMoveが偶数のとき、xIsNext === falseはcurrentMoveが奇数のときであることに気づくかもしれません。言い換えると、currentMoveの値がわかれば、xIsNextがどうあるべきかは常に判断できます。
これら両方を状態として保存する理由はありません。実際、冗長な状態は常に避けるように努めてください。状態に保存するものを簡素化することで、バグを減らし、コードを理解しやすくすることができます。Gameを変更して、xIsNextを別の状態変数として保存せず、代わりにcurrentMoveに基づいて判断するようにしましょう:
もうxIsNextの状態宣言やsetXIsNextの呼び出しは必要ありません。これで、コンポーネントのコーディング中にミスを犯しても、xIsNextがcurrentMoveと同期しなくなる可能性はなくなりました。
まとめ
おめでとうございます!あなたは以下の機能を持つ三目並べゲームを作成しました:
- 三目並べをプレイできる
- プレイヤーがゲームに勝利したときに表示する
- ゲームの進行に合わせて履歴を保存する
- プレイヤーがゲームの履歴を確認し、以前の盤面の状態を見られるようにする
よくできました!これで、Reactの仕組みについて十分な理解が得られたと感じていただければ幸いです。
最終結果はこちらで確認できます:
もし時間が余っているか、新しく学んだReactのスキルを練習したい場合は、三目並べゲームに加えられる改善案をいくつか紹介します。難易度順にリストアップします:
- 現在の手番についてのみ、「あなたは手番 #… です」と表示し、ボタンではなくします。
Boardを書き直して、マス目をハードコードする代わりに2つのループを使って生成します。- 手番のリストを昇順または降順でソートできるトグルボタンを追加します。
- 誰かが勝った場合、勝利の原因となった3つのマス目をハイライトします(そして誰も勝たなかった場合、引き分けの結果についてのメッセージを表示します)。
- 手番履歴リストに、各手番の位置を(行, 列)の形式で表示します。
このチュートリアルを通して、要素、コンポーネント、props、stateといったReactの概念に触れてきました。ゲーム構築においてこれらの概念がどのように機能するかを見てきたので、次はReactでの思考法をチェックして、同じReactの概念がアプリのUI構築においてどのように機能するかを見てみましょう。
