i-Vinci TechBlog
株式会社i-Vinciの技術ブログ

React入門 第2回 state, propsって何ですか?

こんにちは!元自衛官エンジニアの藤田です!
2021年もそろそろ終わりそうですが、皆さんいかがお過ごしでしょうか?
私は息子に「Nizi〇の女の子が履いているコロコロの靴が欲しい」と言われ、YouTubeの動画を中心に調査を進めています。
現在の進捗としましては、「縄跳びダンス」をマスター致しました!
果たしてクリスマスまでにプレゼントの特定-購入まで至ることができるのでしょうか?

さて、そんなわけでReact入門記事の第2回として、state, propsについて書いていきたいと思います。

  1. Webpackを使ってみよう(前回)
  2. State, Propって何ですか? <今回はここ!!!>
  3. Hooks APIって何なんですか?いったい!
  4. Class Component vs. Functional Component 両方の書き方を試してみよう
  5. コンポーネントのライフサイクルって、ちょっと何ですか?え、なに?
  6. Material UIって?ちょっと何がしたいの?
  7. TypeScriptで書いてみよう
  8. Styled Componentで書いてみよう
  9. Redux, Redux-Saga、って何ですか?イヤイヤ、こわっ!
  10. ちょっとアプリを作ってみよう(3~4回程度に回を分けるかもです)

数独アプリを作成する

本入門シリーズを執筆するにあたり、公式ドキュメントの写経になってしまうのは、読んでいる人にとっても自分にとっても退屈だろうなと考えました。
なのでシリーズを通して1つのサンプルアプリを作成しながら、私自身がReactについての理解を深められるような形にしたいなと思います。そっちのほうが読者の方々も退屈しないのではないかなぁと考えたわけです。

しかし、この手の入門何たらでよくある「Todoリスト」の作成は「いい加減やめよーよ」と思っています。
なんかいいお題になりそうなものはないかなぁとRapidAPIのページをうろついていると、良い感じのものがあったので本入門シリーズでは「数独」のアプリを作っていきたいと思います。

作成したコードなどは、GitHubのこちらのリポジトリに載せていきます。

次のような機能を乗せていきましょう。

  1. ログイン
  2. 数独ゲームプレイ機能
  3. 新規ゲーム開始機能
  4. プレイ結果保存機能(どのくらいの時間で解決できたかなど)
  5. ランキング表示機能

とりあえずこんな感じかなというノリでいくつか機能を上げてみました。
ひとまずここら辺の機能を実装していって、余裕ができたら他の新しい機能を足していってみましょう。

本入門シリーズのゴール

作成したReactのアプリをAWSを使っていい感じにデプロイして公開するところまでやります。

  1. Reactアプリ(Front)の作成
  2. Lambda関数(API)の作成
  3. CloudFormationを使用してリソースの定義
  4. CodePipeline + GitHubを使用してCI/CDの構築

ログイン周りはAmplifyを使ってしれっと作ろうと考えています。(あまり詳しくないのですが、なんかよさげという噂なので)
ここら辺のサンプルがあると色々と他の開発にも使えそうなので、入門としてはいいのかなと考えています。

宣言的なView

Reactの特徴として「宣言的」なViewというようなことが公式のページで謳われています。
state, propsを理解することは、すなわち宣言的なViewをどのように作るのか、ということを理解することになるのかな、と私は考えています。

state, propsとは

  1. state:コンポーネントが持つデータ
  2. props: 親のコンポーネントから渡されるデータや関数、コンポーネント
  3. Reactが「宣言的」なViewであるとは、「stateやpropsがある状態・データの場合に表示されるべき画面を定義する、というアプローチで画面を作ることが可能である」ということ

では、実際に作って一緒に理解を深めていきましょう。

準備

環境変数の設定方法

APIの差し先などを環境変数で設定できるように準備しましょう。
package.jsonで定義したbuildコマンドやdevコマンドを次のようにします。

"dev": "env HOGE=hoge webpack serve --open"

また、webpack.config.jsに次のpluginsを設定します。

plugins: [ new webpack.EnvironmentPlugin(['HOGE']) ],

こうすることでwebpackでの「ビルド時」にprocess.env.HOGEとすればhogeが読み込めるようになり、ブラウザでの実行時に環境変数を用いることができます。
こんな感じです。
詳しくはこちら

eslint

開発を本格的に始める前にリンターを入れましょう。

npm i eslint --save-dev
npx eslint --init

コンソールに表示されるガイドに従って設定を決めていきましょう。
設定したリンターが吐いたエラーを潰して一通りエラーがなくなったら、package.jsonに定義したとおりに

npm run dev

を行ってローカルで動かしてみましょう。
正しく動いていれば本格的な開発準備の完了です。

この段階でのコミットはこちらです。
ここからは、記事ごとにブランチを切ってmainが最新の開発状況、という形にしたいと思います。

コンポーネントの作成

まずは、数独のゲームボードを表示するコンポーネントと新規ゲーム開始・プレイ結果保存・ランキング表示などのボタンを表示するコンポーネントを作成していきましょう。
とりあえず必要そうなコンポーネントを書いてみます。

const App = () => {
  return (
    <div>
      <SudokuBoard />
      <MenuButtons />
    </div>
  );
};

SudokuBoardコンポーネントがゲームの本体で、MenuButtonsが色々なメニューボタンを表示するコンポーネントです。
今の段階だとエラーを吐くので製造を進めるにあたり気になる人は、'hogehoge'や'piyopiyo'などの意味のない文字を返すようにそれぞれのコンポーネントを作って進めましょう。例としてMenuButtonsコンポーネントを張っておきます。

const MenuButtons = () => {
  return (
    <div>piyopiyo</div>
  );
};

この段階でのコミットはこちら
ブランチをfeature/react-beginner-002に変更して製造を進めていきます。

SudokuBoardの作成

とりあえず問題を表示して入力ができるようになるところまで作成します。
数独の表は3x3のセルを持つブロックが9つあります。
今回は、3x3のセルをSudokuCellコンポーネント、9つのブロックをSudokuBlockコンポーネントとして作成します。

...という感じで入力できるところまで作成したのがこちらです。

動かしている画面のスクショがこちら。

問題として与えられたデータは青色で表示して、入力された値は薄い黒色で表示しています。

3つのコンポーネントを作成しました。

SudokuBoard.jsx

// APIから受け取った数独の文字列を使いやすい形にして各ブロックに展開する
const sudokuData = '...465......2..7..9....76..6....234..15...2.9.4...8........6..17.1...9.3..9...5..';
//  ... 465 ...
//  ... 2.. 7..
//  9.. ..7 6..
//  6.. ..2 34.
//  .15 ... 2.9
//  .4. ..8 ...
//  ... ..6 ..1
//  7.1 ... 9.3
//  ..9 ... 5..

const SudokuBoard = () => {
  // 各ブロックに渡すデータの配列。各要素は{ value: x, fixed: true/false }。APIから渡された初期値はfixed: true
  const [blocks, setBlocks] = useState([]);

  // APIから受け取った数独のデータを良い感じに並び変える
  useEffect(() => {
    const _blocks = [[], [], [], [], [], [], [], [], []];
    for (let i = 0; i < sudokuData.length / 3; i += 1) {
      const rawdatas = sudokuData.slice(i * 3, (i + 1) * 3).split('');
      const datas = rawdatas.map(d => {
        const isNumberData = Number.isFinite(parseInt(d, 10));
        return {
          value: isNumberData ? d : '',
          fixed: isNumberData,
        }
      });
      const idx = Math.floor(i / 9) * 3 + (i % 3);
      _blocks[idx].push(...datas);
    }
    setBlocks(_blocks);
  }, []);

  const onChangeCellInput = (event, blockIdx, blockCellIdx) => {
    event.stopPropagation();
    const value = event.target.value;
    const newBlocks = blocks.slice();
    newBlocks[blockIdx][blockCellIdx] = { value, fixed: false };
    setBlocks(newBlocks);
  };

  // SudokuBlockコンポーネントに展開
  const rows = blocks.reduce((acc, block, idx) => {
    const rowIdx = Math.floor(idx / 3);
    acc[rowIdx].push(
      <SudokuBlock
        key={sudokublock_${idx}}
        blockIdx={idx}
        block={block}
        onChangeCellInput={onChangeCellInput}
      />
    );
    return acc;
  }, [[], [], []]);

  // SudokuBlockコンポーネントを各行に3つずつ並べる
  const content = rows.map((row, idx) => (
    <div
      key={bulockrow_${idx}}
      className={'board_row'}
    >
      {row}
    </div>
  ));

  return (content);
};

SudokuBlock.jsx

const SudokuBlock = (props) => {
  const {
    blockIdx,
    block,
    onChangeCellInput
  } = props;

  const rows = block.reduce((acc, data, idx) => {
    const rowIdx = Math.floor(idx / 3);
    acc[rowIdx].push(
      <SudokuCell
        key={sudokucell_${blockIdx}_${idx}}
        blockIdx={blockIdx}
        blockCellIdx={idx}
        data={data}
        onChangeCellInput={onChangeCellInput}
      />
    );
    return acc;
  }, [[], [], []]);
  const content = rows.map((row, idx) => (
    <div key={cellrow_${blockIdx}_${idx}}>
      {row}
    </div>
  ));
  return (
    <div className={'sudoku_block'}>
      {content}
    </div>
  );
};

SudokuCell.jsx

const SudokuCell = (props) => {
  const {
    blockIdx,
    blockCellIdx,
    data,
    onChangeCellInput,
  } = props;

  return (
    <input
      className={classNames('sudoku_cell_input', {
        ['sudoku_cell_input__fixed']: data.fixed,
      })}
      type="number"
      value={data.value}
      min="1"
      max="9"
      step="1"
      readOnly={data.fixed}
      onChange={(event) =>
        onChangeCellInput(event, blockIdx, blockCellIdx)}
    />
  );
};

解説

state, propsの点で解説をしたいと思います。

stateの宣言

まずは、ゲームの状態を保持する必要があります。そのためにState Hookを利用して、3x3のブロックに渡すデータの配列をblocksとして宣言しています。(該当箇所:SudokuBoard.jsx#Line20-21)

この宣言によりblocksというstateとstateへのsetterを使用できるようになります。
stateにどのようなデータを持てばいいか、今回は入力データが存在することと問題として与えられたデータは編集・更新出来ないようにすること、という2点で考えました。
数独の表に表示する各データの値と編集・入力可能が分かればいいかな?という考えです。

Effect Hook

Effect Hookを利用してblocksに初期値を設定しています。
Effect Hookやその他のHooks APIについては次回の記事で触れたいと思います。

state, props

SudokuBoardで宣言したstateを子コンポーネントであるSudokuBlock, SudokuCellにpropsとして渡しています。
propsとして渡されたblocks stateは、SudokuCellコンポーネントで以下の二つの方法で利用しています。

  1. 入力データの表示(該当箇所:SudokuCell.jsx#Line27)

  1. 表示のスタイリング(入力された値を青色に)(該当箇所:SudokuCell.jsx#Line24)

しれっとclassnamesを使用しています。詳細はこちら

また、blocks stateと一緒に宣言されたsetBlocksというsetterは、関数の中でstateの更新に利用されています。(該当箇所:SudokuBoard.jsx#Line46)

この関数は、propsとして子コンポーネントに渡され、input JSXで値が変更されるイベント(onChange)が起きた時に利用します。(該当箇所:SudokuCell.jsx#Line33)

子コンポーネントにどのようなpropsを渡せばいいか、今回はonChangeでどのようなデータを利用すればblocksの更新処理がうまく書けるかなという視点で決めていきました。
開発が進んで画面のこだわりや仕様が増えてきて、子コンポーネント側でさらなるデータが必要となった時に、渡すpropsも増えてくると思います。

input JSXにpropsとして渡されたonChangeは、Reactでよく使う属性です。
通常のDOM(その意味するところに深い意味はないです)とReactのDOMでは、属性の挙動に若干の違いがあったりします。(DOMについてのドキュメント)
onChange属性についてはそれほど差異がないらしいです。(onChangeについて)

コンポーネントそのものをchildren propsとして渡すこともできます。(props.children)

stateとpropsについては、以下のような特徴があります。

  1. stateはコンポーネント自身が保持する。変更できるのはsetterを通してのみ。
  2. propsは通常親コンポーネントから渡される。変更できない。
  3. stateやpropsの更新を検出して再描画が必要かReactが判断する(reconciliation(更新検出処理))

まとめ

入門シリーズとして数独のアプリを作ることにしました。
今回のテーマであるstateとpropsは、Reactを触っていると常にどのように扱うか考えないといけないことです。
私も何がベストなのかなと模索しながら作成しているのですが、皆さんの参考になれば嬉しいです。

次回

次回は、APIを叩いて問題を取得するところとゲームの達成判定処理、新規ゲーム開始などのメニューの実装を進めたいと思います。
そこらへんが出来てくると一気にゲーム感が出るかもですね!!
テーマとしては、Hooks APIです。