ぺんぎん

React入門1

公開日:2024/07/28(sun)

React公式チュートリアルを実践!フロントエンドではお馴染みになりつつあるReactを勉強します!今回はチュートリアルのゲームが一旦完成する部分まで作成します!

1. Reactについて

1.1 Reactとは?

ReactとはJavaScriptのオープンソースフレームワーク・ライブラリの1つ。Meta社(旧Facebook)が2011年に社内用で開発していたライブラリを2013年に一般公開されたもの。
React 公式サイト
旧React 公式サイト
現在では日本でもフロントエンド開発の主流となっている。

1.2 Reactの特徴

コンポーネント化によるUI作成

Reactの一番の特徴はUIをコンポーネントを利用して開発を行う点である。Webサイトにおける1部分をリンクボタン、説明文、動画など同じ内容を繰り返し使うものをコンポーネント化し、それらを組み合わせて作り上げていく。これにより、コードの再利用性が高まり、保守性が向上する。

マークアップとコードからコンポーネントを作成

JSXと呼ばれるマークアップ構文を用いて開発を行う。JSXはHTMLと同様にコードを書くことができるため追加の技術取得をあまり必要としない。また、ReactコンポーネントはあくまでJavaScriptの関数と定義するためIf文などを用いて、条件分岐の表示の対応がしやすい。

仮想DOM

Reactは仮想DOM(Virtual Document Object Model)を用いて、実際のDOMとの差分を計算し、最小限の更新を行うことができる。これにより、パフォーマンスの向上が期待できる。

フレームワークの利用

Reactはあくまでライブラリであり、コンポーネントの組み合わせが可能であるが、外部からのデータ取得などの指定がなあい。その場合Next.jsなどフルスタックのフレームワークを使うことであらゆるUIを作成できる。

あらゆるプラットフォーに対応

ReactはJavaScriptコードを段階的にロードすることにより、ウェブアプリの素早い読み込みを実現している。そのため、多くのウェブブラウザでの動作が保証されている。
また、同Meta社が開発したReactNativeの利用により、クラスプラットフォームアプリ開発、すなわち、iOSアプリ、Androidアプリの作成が可能である。

2. Reactの基本

2.1 Reactチュートリアルについて

公式サイトのドキュメントとして公開されている三目並べを作成する。また、ここではHTMLとJavaScriptについて多少慣れていることを前提として作られているため随時JavaScriptドキュメントなどの活用が考えられる。
React公式 チュートリアル:三目並べ
旧React公式 チュートリアル:三目並べ
以上サイトを参照し、Reactの基本を学ぶ。旧サイトは現在更新されていないが、一部説明などが詳しいため参照している。

2.2 Reactの環境構築

Reactは本来ローカル環境を構築し、開発を行う。その手順を簡易的に示すが、今回はウェブブラウザ上でコード記述を行い実行を確認する。
環境構築手順(旧公式サイト)
1. Node.jsのインストール
2. 以下コマンドの実行により新規プロジェクトを作成
*my-app*はプロジェクト名が入る。

npx create-react-app *my-app*

3. 以下コマンドによりプロジェクトを起動

cd *my-app*
npm start

4. ブラウザでlocalhost:3000にアクセスし、Reactの初期画面が表示されれば成功

2.3 チュートリアルの環境構築

チュートリアルではCodeSandBoxというサイトを利用してコーディングを行う。
CodeSandBox

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

今回は内容の関係でゲームが一旦完成し、終了宣言をかける部分までとなる。

チュートリアルで公開されている完成形

3.1 チュートリアルのセットアップ

以下のサイトにアクセスし、セットアップ済みのコードに追加でコーディングを行う
セットアップ済みのプロジェクト
このサイトではすでにプロジェクトが作成されており、Browserに表示されるのはDevelopされたものとなる。
CodeSandboxの画面は次のようになっている。
CodeSandboxのスクリーンショット
画面左にあるのはファイルセクションであり、App.jsやstyle.cssなどファイルが表示されている。ここには各種ファイルを追加する際に表示される。画面中央はコードエディタであり、開いているコードを確認・編集が可能。画面右側はBrowserセクションであり、コードを実行した際の表示がされる。

3.2 スタートアップコードの確認

App.js

export default function Square() {
    return <button className="square">X</button>
}

1行目はSquare関数の定義を行っている。exportというJavaScriptのキーワードはこの関数を外部からアクセスできるようにするもの。defaultというキーワードは他ファイルから参照の際この関数がこのファイルのメイン関数であることを示している。

style.css

すでに習得済みとして説明を省略する。ただし、Reactではclassで定義されたものには割り当てられない。classNameで定義されているものに対してstyle.cssが割り当てられる。

index.js

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './styles.css';

import App from './App';

今回のチュートリアルにてこのファイルは編集しない。しかし、今ファイルはApp.jsなどコンポーネントと、ウェブブラウザの橋渡しをする最も重要なファイルである。
このファイルにて以下の4つの部品の取り出しをおこなっている。

これらの部品を呼び出し、まとめ、Public内のindex.htmlに入れることにより、JavaScriptでの記述をWebブラウザで表示できる。

3.3 三目並べの盤面の作成

2マス目を作る

三目並は盤面が9マス必要ですが、1マスしかありません。単純にbuttonタグをコピーペーストし実行しようとすると次のようなTypeエラーが出ます。

Cannot assign to read only property 'message' of object 'SyntaxError: /src/App.js: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>? (3:2)

これはReactコンポーネントの性質あり、コンポーネが返す(Return)の値は1つにまとめられたものである必要があるためである。buttonのJSX要素を複数返したい場合はフラグメントと呼ばれる「<>」、「</>」で囲うことによりこの複数の要素の値を返すことができる。
修正したプログラムと実行結果は次のとおりになる。

export default function Square() {
    return (
        <>
            <button className="square">X</button>
            <button className="square">X</button>
        </>
    );
}

2マス目の表示のための修正後

2マス目以降の作成

前のようにコピーペーストを続ければ9マスを作ることができるのですが、しかしこのままであると9マス分のコードが必要となりますし、buttonの子要素にはXが入っていますが○になる場合もありその度にどこの行の処理が必要か考えるのは大変である。そこでReactの強みであるコンポーネントを作成することで再利用可能で可読性の高いものにする。

初めに関数名を次のように変え、今から作るものがSquareを組み合わせてできたBoardを作るものとして関数名を変える。

export default function Board() {
    //......
}

次にSquare関数を先ほどのButtonで作成しそれを、Board関数内で呼び出す。ここでboard-rowというクラスを与えているが、このクラスにはボタンを折り返して表示させるた目の親要素のStyleが割り当てられている。

function Square() {
    return <button className="square">1</button>
}

export default function Board() {
    return (
        <>
            <div className="board-row">
                <Square />
                <Square />
                <Square />
            </div>
            <div className="board-row">
                <Square />
                <Square />
                <Square />
            </div>
            <div className="board-row">
                <Square />
                <Square />
                <Square />
            </div>
        </>
    );
}

Square関数の定義時、先頭にexport defaultを入れてはいけない。入れてしまうことにより前述したとおり、App.jsを読み込んだ際、メイン関数であると認識されてしまうためである。

Board関数内にて別コンポーネントの呼び出しはコードのようになる。ここでは関数名(コンポーネント名)の先頭は大文字になるという点に注意が必要である。また、ファイルをまたいでの呼び出し時には次のように先頭にて該当ファイルの読み込みを行う必要がある。

import Square from './Square';

このままでは全てのマスが1で固定されてしまうため、Squareコンポーネントの呼び出しの際に値を指定したい。そのためSquare関数にプロパティを入力できるようにする。この方法はHTMLにJavaScriptやFlaskを用いたPython WebAPPの開発時と同じ方法である。

function Square({value}) {
    return <button className="square">{value}</button>
}
export default function Board() {
    return (
        <>
            < div className="board-row" >
                < Square value="1" />
                < Square value="2" />
                < Square value="3" />
            < /div>
            /....
        < />
    );
}

このようにpropsを引数に取り、その中のvalueを表示することで、Board関数内でSquare関数を呼び出す際に値を指定することができる。

3.3 盤面での処理の追加

インタラクティブなコンポーネントの設計

簡単な処理として各ボタンを押すたびにそれがXに変わる動作を設計する。Square関数の中にhandleClick関数を定義し、呼び出しのpropsにonClickを追加する。

function Square({value}) {
    function handleClick() {
        console.log('clicked!');
    }
    return 
        <button className="square" onClick={onClick}>
        {value}
        </button>
}

このプログラムでの動作を確認する。Consoleを開いた状態でかく番号を押すたびにclicked!というように出力されていれば問題ない。
ここでわかるように各このポーネンとの中で関数を新たに定義することができその関数を利用した動作を出力させることができる。

ここで、handleClick関数をSquare関数の外で定義したプログラムを試す。

function Square({value}) {
    return
        <button className="square" onClick={handleClick}>
        {value}
        </button>
}
function handleClick() {
    console.log('clicked!');
}

実際に動作を確認してみたところ先ほどと同様の処理が行われていることがわかった。このようにコンポーネント外での関数定義にも対応していることがわかった。しかしコードの可読性や、使い回し、保守を考えた時必要なコンポーネントを必要な場所で記載したほうが良いのではないかと個人的には考えた。

次にSquareコンポーネントにてクリックされたことを記憶し、"X"を表示する機能を実装する。「記憶」するためにはReactコンポーネントのstateというものを利用する。
ReactはuseStateと関数の提供があり、これを利用することでコンポーネント内で状態を保持することができる。useStateは配列を返し、その中に状態の初期値を入れることができる。また、useStateは2つの要素を返す。1つ目は状態の値、2つ目は状態を更新するための関数である。

ファイルの先頭にてuseStateをインポートしSquare関数からvalueのpropsプロパティを削除する。その代わりに2つの要素の配列にてuseStateを呼び出す。

import { useState } from 'react';

function Square() {
    const [value, setValue] = useState(null);

    function handleClick() {
    //...
}}

valueがstateの現在の値を格納し、setValueはその値の更新に使う関数ということになる。useStateにわたすNullはstate変数の初期値として使用するため今回の場合valuenullという値から処理が行われる

次にhandleClick関数内でsetValueを使い、クリックされた際にvalueの値を変更する。ここでは"X"を表示するようにする。
値の変更には先ほど定義しているsetValue関数を使用し以下のような記述を行う。

また、コンポーネント自体がpropsの受け取りを行わないため、Boardコンポーネントので呼び出されるSquareコンポーネントのpropsを削除する。

import { useState } from 'react';
function Square() {
    const [value, setValue] = useState(null);

    function handleClick() {
        setValue('X');
    }
    return(
        <button className="square" onClick={handleClick}>
        {value}
        </button>
    );
}

export default function Board() {
    return (
        <>
            < div className="board-row" >
                < Square />
                < Square />
                < Square />
            </div>
            /....
        </>
    );
}

このようにクリックされた際に"X"が表示されるようになった。このようにReactではコンポーネントの再利用性が高く、コードの可読性が高いため、保守性が高いコードを書くことができる。

3.4 ゲームの状態を管理する

ゲームのデータ保持にはいくつか方法がある。ここで考えなければいけないのは"○"と"X"の処理があるという点だ。それぞれどこの盤面でどちらが入力されているかを先ほどのようにSquareコンポーネント内で保持する方法が最初に考えられる。しかしこの方法は今後ゲームの終了処理を行う際複数の値をSquareコンポーネントに移動し確認する処理が必要となり現実的ではない。よってこの方法は使わないこととする。

別の方法としてBoardコンポーネントにてどのマスがどの状態であるかを一括で管理(配列を使った管理)にすることで終了処理の実装も比較的容易に実装可能である。よってこの方法を採用する。

まずはBoardコンポーネントにてstateを定義し、その中に9つのマスの状態を保持する配列を定義する。squaresという名前のstate変数を宣言し、デフォルト値はnullを返す。

//...
export default function Board() {
    const [squares, setSquares] = useState(Array(9).fill(null));
    return (
        //...
    );
}

この変更により、Squareコンポーネントにpropsにて値を渡し、それを出力するように変更する。

import { useState } from 'react';
function Square({value}) {
    return (
        <button className="square">{value}</button>
    );
}
export default function Board() {
    const [squares, setSquares] = useState(Array(9).fill(null));
    return (
        <>
            <div className="board-row" >
                < Square value={squares[0]}/>
                < Square value={squares[1]}/>
                < Square value={squares[2]}/>
            </div>
            /....

        </>
    );
}

このようにSquareコンポーネントにてpropsにて値を渡し、それを出力するように変更する。

このように複数の子コンポーネントからデータを収集したり2つの子コンポーネント同士での通信を行う場合親コンポーネントに共有のstateを宣言する。子コンポーネントにはpropsにてデータのやり取りができ、この方法によりコンポーネントの同期を行うことができる。

ここでは値の変更処理をおこなっていないため初めに何も表示されず、ボタンを押しても何も表示されない。

クリックされた際の処理

初めにクリックされた時、Xを表示するようにする。 ここで、どのマス目が埋まっているかを管理しているのはBoardコンポーネントであり、SquareからBoardのstateを更新する手段が必要となる。あくまでstateはそれを定義するコンポーネントにプライベートなものであるため、SquareからBoardのstateは変更することができない。
代わりとして、BoardコンポーネントからSquareコンポーネントに関数をわたすことにより、マス目がクリックされた時にSquareにその関数を呼び出してもらうようにする。その関数をonSquareClickとする。

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

次にBoardコンポーネントにてhandleClickとして、盤面の情報を更新する関数を定義し、それをSquareコンポーネントに渡す。

export default function Board() {
    const [squares, setSquares] = useState(Array(9).fill(null));
    function handleClick() {
        const newSquares = squares.slice();
        nextSquares[0] = 'X';
        setSquares(nextSquares);
    }
    return (
        //...
    );
}

handleClick関数は初めに配列のコピーを行い次の配列の準備をする。次に配列の中身をへこうする処理を行いその配列をsetSquaresに通し、盤面情報を管理する配列を更新する。

export default function Board() {
    const [squares, setSquares] = useState(Array(9).fill(null));
    function handleClick(i) {
        const newSquares = squares.slice();
        newSquares[i] = 'X';
        setSquares(newSquares);
    }
    return (
        //...
    );
}

次にSquareコンポーネントにてこの関数を呼び出しは次のようにする。

<Square value={squares[0]} onSquareClick={handleClick(0)} />

しかし、このプログラムでは正しく動作しない。onSquareClickに関数を渡すつもりが、handleClick(0)の実行値を渡す式となってしまっている。これを解決し、ユーザーがクリックしたときに、正しく関数が動くようにするために、handleClick(0)を呼び出すための関数を次のようにつくる。

<Square value={squares[0]} onSquareClick={() => handleClick(0)} />

()=>はアロー関数と呼ばれ、関数名を命名しない無名関数の1つ。ここではSquareにアローの無名関数を渡し、その関数を実行することにより実質的にhandleClick(i)を実行していることとなる。他のマスも同様にすることで正しく処理ができる。

ここまでのコード一は次のようになる。

import { useState } from "react";
function Square({ value, onSquareClick }) {
    return (
        <button className="square" onClick={onSquareClick}>
        {value}
        </button>
    );
}
    
export default function Board() {
    const [squares, setSquares] = useState(Array(9).fill(null));
    function handleClick(i) {
        const nextSquares = squares.slice();
        nextSquares[i] = "X";
        setSquares(nextSquares);
    }
    return (
        <>
            <div className="board-row">
                <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
                <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
                <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
            </div>
            <div className="board-row">
                <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
                <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
                <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
            </div>
            <div className="board-row">
                <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
                <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
                <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
            </div>
        </>
    );
}

○を表示する

今の状態ではXしか入力できないため○の入力の処理を作成する。先手を"X"とし、現在の手番の追跡を行うstateを追加する。

export default function Board() {
    const [squares, setSquares] = useState(Array(9).fill(null));    // 盤面の状態
    const [xIsNext, setXIsNext] = useState(true);                   // 次の手番が"X"かどうか

    //...
}

xIsNextはプレイヤーが着手するたびに反転するためheadClick関数にて処理を行えば良い。

export default function Board() {
    //...
    function handleClick(i) {
        const nextSquares = squares.slice();
        if (xIsNext) {              // 次の手番が"X"の場合
            nextSquares[i] = "X";   
        } else {                    // 次の手番が"O"の場合
            nextSquares[i] = "O";
        }
        setSquares(nextSquares);    // 盤面の状態を更新
        setXIsNext(!xIsNext);       // 次の手番を更新
    }
    //...
}

動作結果は次のようになる。クリックするごとに◯とXが入れ替わることが確認できる。

ここで1つ問題がある。すでに入力された部分を再度入力することで上書きができてしまう。三目並べという同じパターンになってしまうゲームのためそのようなルールは面白いが今回はそのようなルールは考えないためすでに入力された部分には入力できないようにする必要がある。
対処として、クリックされた時、そのマスに何かしらの値が入っている場合盤面や手番の更新を行わないことで対応する。

function headClick() {
    if (squares[i]) {
        return;
    }
    const nextSquares = squares.slice();
    //...
}

ここまでのコードは次のようになる。

import { useState } from "react";
function Square({ value, onSquareClick }) {
    return (
        <button className="square" onClick={onSquareClick}>
        {value}
        </button>
    );
}
    
export default function Board() {
    const [squares, setSquares] = useState(Array(9).fill(null));
    const [xIsNext, setXIsNext] = useState(true);
    function handleClick(i) {
        if (squares[i]) {
            // console.log("filled!");
            return;
        }
        const nextSquares = squares.slice();
        if (xIsNext) {  // 次の手番が"X"の場合
            nextSquares[i] = "X";
        } else {        // 次の手番が"O"の場合
            nextSquares[i] = "O";   
        }
        setSquares(nextSquares); // 盤面の状態を更新
        setXIsNext(!xIsNext); // 次の手番を更新
    }
    return (
        <>
            <div className="board-row">
                <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
                <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
                <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
            </div>
            <div className="board-row">
                <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
                <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
                <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
            </div>
            <div className="board-row">
                <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
                <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
                <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
            </div>
        </>
    );
}

終了処理

前編のゴールとして終了の処理を実装する。9つのマスの中で縦・横・斜めのいずれかにおいて3連続並べることで勝利を確定することができる。
勝利判定として8パターンあるので、それぞれのマスの番号を配列として渡し、その値全てが等しい時に勝利とすれば良い。この処理を行う関数をcalculateWinnerとして定義する。

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

この関数はReact特有ではないためあまり重要視しない。ただし、この判定方法はゲームなどにおいては多く用いる場面が多い。

この関数の呼び出しはhandleClick関数内で読み込み追加入力ができないようにすると同時に勝利宣言をする。

function handleClick(i) {
    if (squares[i] || calculateWinner(squares)) {
        return;
    }
    const nextSquares = squares.slice();
    //...
}

勝利した者のUI表示

現在の状態の出力のために新たにdivを用意し、その中にステータス状態を入れることで文字の出力を考える。

export default function Board() {
    //...
    const winner = calculateWinner(squares);
    let status;
    if (winner) {
        status = "Winner: " + winner;
    } else {
        status = "Next player: " + (xIsNext ? "X" : "O");
    }
    return (
        <>
            <div className="status">{status}</div>
            <div className="board-row">
                //...
    )
}

このように勝利者がいる場合、その勝利者を表示するようにする。このようにすることで勝利者が表示されるようになる。

これで今回の目標であるゲーム自体の作成に到達できた。この段階でのプログラムを示す。

import { useState } from "react";
function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}

export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));
  const [xIsNext, setXIsNext] = useState(true);
  function handleClick(i) {
    if (squares[i] || calculateWinner(squares)) {
      console.log("win!");
      return;
    }
    const nextSquares = squares.slice();
    if (xIsNext) {
      // 次の手番が"X"の場合
      nextSquares[i] = "X";
    } else {
      // 次の手番が"O"の場合
      nextSquares[i] = "O";
    }
    setSquares(nextSquares); // 盤面の状態を更新
    setXIsNext(!xIsNext); // 次の手番を更新
  }
  const winner = calculateWinner(squares);
  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }
  return (
    <>
      <div className="status">{status}</div>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
      </div>
    </>
  );
}

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

まとめ

以前Reactを触ったことがある筆者が全てを理解しながら大体1時間ほどで学ことができた。Reactには今回触れた内容以外にもReactならではの部分があり今後はそれに触れることができれば良いと思う。

React入門において公式チュートリアルはかなり良いものであった。あまりブログなどでチュートリアルが取り上げられておらず学ぶ方法も限られているが広く普及されれば良いなと思っている。

次回はこの続きである過去に戻る処理を追加したい。また今後実際にフロントエンドでの作業方法なども勉強できれば良い。

本コンテンツの作成時間(HTML/CSS/JavaScriptの設計・実装を含む):約16時間