めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

バックエンドエンジニアのための(かどうかは本当はよく分からないけど、とにかく書いてみる)React 入門(パート5)

何の話かと言うと

enakai00.hatenablog.com

上記の続きになります。次のステップとして、ボードの上にコマを置いて表示する機能を実装しますが、段階的に進めていきましょう。まずは、コマの情報を保存する状態変数を用意して、この内容に応じてコマを表示する部分まで実装します。

状態変数の利用

コマの情報を保持、表示するために、前回作成した Board.js を次のように拡張します。

     1  import React, { useRef } from "react";
     2  import { Box } from "@chakra-ui/react";
     3
     4  import board from "../assets/board.png";
     5  import black from "../assets/black.png";
     6  import white from "../assets/white.png";
     7  import blank from "../assets/blank.png";
     8
     9
    10  const Cell = (props) => {
    11    const x = 25 + 67 * props.pos_x;
    12    const y = 28 + 67 * props.pos_y;
    13
    14    const style = {
    15      position: "absolute",
    16      width: "65px",
    17      height: "65px",
    18      left: x,
    19      top: y,
    20    };
    21
    22    var element;
    23    switch (props.mark) {
    24      case "black":
    25        element = (
    26          <img src={black} alt="black" style={style} />
    27        );
    28      break;
    29      case "white":
    30        element = (
    31          <img src={white} alt="white" style={style} />
    32        );
    33      break;
    34      default:
    35        element = (
    36          <img src={blank} alt="blank" style={style} />
    37        );
    38    }
    39    return element;
    40  }
    41
    42
    43  const getField = (size) => {
    44    const field = new Array(size);
    45    for (let y = 0; y < size; y++) {
    46      field[y] = new Array(size);
    47      field[y].fill("blank");
    48    }
    49    field[3][3] = "white";
    50    field[3][4] = "black";
    51    field[4][3] = "black";
    52    field[4][4] = "white";
    53    return field;
    54  }
    55
    56
    57  export const Board = (props) => {
    58    const size = 8;
    59    const fieldRef = useRef(getField(size));
    60    const field = fieldRef.current;
    61
    62    const fieldElements = [];
    63    for (let y = 0; y < size; y++) {
    64      for (let x = 0; x < size; x++) {
    65        fieldElements.push(
    66          <Cell pos_x={x} pos_y={y} mark={field[y][x]} />
    67        );
    68      }
    69    }
    70
    71    const style = {
    72      position: "relative",
    73      width: "584px",
    74      height: "592px",
    75      backgroundRepeat: "no-repeat",
    76      backgroundPosition: "center",
    77      backgroundImage: `url(${board})`,
    78      backgroundSize: "100%"
    79    };
    80
    81    const element = (
    82        <Box style={style}>
    83          {fieldElements}
    84        </Box>
    85    );
    86
    87    return element;
    88  }

43行目からの関数 getField では、8×8 のマス目に対応した2次元配列 field を用意して、各マス目のコマを "black"(黒)、"white"(白)、"blank"(ブランク)という文字列で表現します。中央部分の2枚ずつ白黒のコマがある初期状態を構成して、これを返却します。

そして、59行目では、この初期状態の2次元配列を関数 useRef に代入しています。useRef は、 一度初期化されると、そのあとは再初期化されずに現在の値を保持し続ける「状態変数」を用意します。関数 Board が初めて実行された時は、59 行目で初期状態の field の値がオブジェクト fieldRef の current 属性に保存されます。その後、関数 Board 内で fieldRef.current の内容(つまり二次元配列 field の内容)を変更して、関数 Board の実行が終わったとします。通常、関数 Board 内で使用した変数の内容はこの時点で失われますが、filedRef の内容は失われずにそのままメモリー上に保持されます。そして、再度、関数 Board を実行すると、59 行目で前回の fieldRef の値が再現されます。イメージとしては、fieldRef は簡易的な外部データベースになっていると考えられます。これにより、ゲームを実行中の盤面の状態を保持することができます。なお、関数 Board 内では、fieldRef.current の値を何度も使用するので、コードを短くするために、60行目で fieldRef.current を変数 field に入れて、この後のコードでは、fieldRef.current の代わりに field を使用しています。

次に、62〜69行目では、配列 fieldElements に Cell コンポーネントを詰め込んでいます。この後で説明するように、Cell コンポーネントは、1つのマス目(白、黒、もしくは、ブランク)を表示するコンポーネントです。属性値 pos_x、pos_y にマス目の位置、属性値 mark にマス目にあるコマを指定して、画面上の対応する位置に対応するコマの画像を表示します。これを83行目で、(前回までは空だった)Box コンポーネントの中に配置しています。前回説明したように、波括弧内には Javascript のコードが配置できて、変数を配置した場合、変数の内容に置き換わります。

コマの表示

Cell コンポーネントを定義する関数 Cell は、同じファイル Board.js 内の10〜40行目にあります。ファイルを分けてインポートしても構わないのですが、Board コンポーネントからしか参照しないものなので、実装を隠蔽する意味で、同じファイル内で定義しています。

これも前回説明しましたが、コンポーネントを配置する際に与えた属性値は、引数 props で受け取ります。ここでは、props.pos_x、props.pos_y でコマの位置、props.mark でコマの種類が得られます。これらの値を利用して、インラインスタイルで画面上の表示位置を指定(18、19行目)し、さらに、表示する画像を使い分けています。コマの画像ファイルは、5〜7行目でインポートしており、26、31、36行目にあるように、img 要素の src 属性に直接指定することができます。

これで新しい Board.js の説明は終わりです。この段階でアプリケーションを実行すると、無事に初期状態の盤面が表示されます。

key 属性の割り当て

これで無事に盤面が表示されたようですが・・・実は、ブラウザで JavaScript コンソールを確認すると、次のような警告が表示されています。

react-jsx-dev-runtime.development.js:87 Warning: Each child in a list should have a unique "key" prop.

先ほど、配列 fieldElements に Cell コンポーネントを詰め込んで表示しましたが、配列にコンポーネントを詰め込む際は、それぞれのコンポーネントにユニークな key 属性を指定することが推奨されます。React のレンダリングエンジンは、変更のあったコンポーネントのみを再レンダリングする最適化を行いますが、配列内のコンポーネントが増減した場合、増減する前後のどのコンポーネント同士を比較して変更を検知すればよいかわからなくなります。そこで、コンポーネントを同定するための key 属性が必要になるのです。(逆に言うと、何らかの理由で強制的に再レンダリングしたいコンポーネントがあった場合は、その key 属性の値を変更すればよいことになります。このテクニックは後ほど使います。)今回は、66行目を次のように変更して、コマの座標値をユニークな key として使用します。

    66          <Cell key={y.toString()+x.toString()}
                      pos_x={x} pos_y={y} mark={field[y][x]} />

残念ながら、新しいコマを置く機能にはたどりつきませんでしたが、今回の説明はここまでとします。ここまで実装したコードは、下記のリポジトリ(v0.2 ブランチ)から参照できます。

github.com

おまけ

Javascript では、次のように、波括弧付きのインポートとそうでないインポートがあります。

import React, { useRef } from "react";

ファイル内で関数やクラスを普通(?)に export した場合、波括弧付きのインポートが必要で、export した時と同じ名前でインポートする必要があります。一方、ファイル内で1つだけ、default を指定した export が可能で、これは波括弧無しでインポートできます。また、default export したモジュールは任意の名前でインポートすることができます。

次回予告

次回は、ブランクのマス目に新しいコマを置く機能を実装します。Reversi のルールに従って置けるかどうかの判断は、その後、あらためて実装することにします。

パート6はこちらです。

enakai00.hatenablog.com