Introduction to React.js Part.5 (fixed)

こんにちは、ワタナベ(wtnb_dev)です。

前回の記事の続きです。今回で最終話になります。

Introduction to React.js Part.4

前回までで、画面を構成する各コンポーネントの土台ができましたので、今回は中身を実装していきます。

画面構成はこんな感じです。

画像1

中身を実装していく前に、React.jsの重要な機能である、ステートとpropsについて見ていきます。

ステート(state)について

React.jsでは、コンポーネントごとにステート(state)を持つことができます。

ステートは、そのコンポーネントの状態を管理するために使われ、複数のステートを持つことができます。

早速例を見ていきましょう。

今回作成するAnswerPanelコンポーネントを例に見ていきます。

画像2

AnswerPanelコンポーネントには、回答を入力するテキストボックスと、回答するためのボタンがあります。このうち、"状態"を管理する必要があるのはどれになるでしょう。

ここでは、テキストボックスに入力された値を、"状態"として管理してみます。

AnswerPanel.jsを、以下のように修正してみましょう。

import React, { useState } from 'react';

export default function AnswerPanel() {
  const [inputNum, setInputNum] = useState('');
  
  return (
    <div>
      <input type="text" name="inputNum" value={inputNum} placeholder="数値を入力してください" />
      <button type="button">回答</button>
    </div>
  );
}

ステートを使うために、useState関数をimportしています。

次に、inputNumというステートを格納する変数を定義しています。

ステートの変数は任意に決めることができます。今回は、入力された数値を格納するため、inputNumという名前にしています。また、ステートを定義する際は、併せてステートを更新するための関数(ここではsetInputNum)も定義します。

ステートを更新する際は、このsetInputNumを使用して更新します。

また、useStateの引数には、ステートの初期値(今回は空文字)を設定します。

最後に、inputタグのvalue値に、ステートの値を使用するように修正しています。JSX内で変数の値を扱う際は、中括弧{}で囲みます。

この状態で、一旦ビルドして動作確認してみましょう。

前回同様、以下のコマンドでwebpackでビルドします。

$ yarn run build

ビルドが完了したら、index.htmlをWebブラウザで開きます。

画像3

この状態で、テキストボックスに値を入力してみましょう。

何も入力できないかと思います。

これは、テキストボックスの値が変更された際に、ステートを更新する処理を実装していないため、テキストボックスのvalue値に指定している変数inputNumが、常に空文字のままとなるためです。

それでは、テキストボックスの値が変更された際に、ステートの値を更新する処理を実装していきます。

テキストボックスの値が変更された際のイベントは、inputタグのonChangeイベントで取得できます。

onChangeイベントの実装

AnswerPanel.jsを以下のように修正します。

import React, { useState } from 'react';

export default function AnswerPanel() {
  const [inputNum, setInputNum] = useState('');
  
  const handleInputNumChange = e => {
    setInputNum(e.target.value);
  }
  
  return (
    <div>
      <input type="text" name="inputNum" value={inputNum} placeholder="数値を入力してください"
        onChange={handleInputNumChange} />
      <button type="button">回答</button>
    </div>
  );
}

関数handleInputNumChangeを新たに定義し、inputタグのonChangeイベント発生時に、この関数を呼び出すようにしています。

関数handleInputNumChange内では、setInputNum関数を使用して、ステートの値を更新しています。引数eはonChangeイベントが発生した際の情報が格納されており、e.target.valueでinputタグに入力された値を取得することができます。

これで、テキストボックスの値が変更されたタイミングで、同時にステートも更新することができるようになります。

再度webpackでビルドし、実行してみましょう。

$ yarn run build

画像4

今度はちゃんと、値が入力できるかと思います。

何のためにステートを使うのか

では何のためにステートを使うのでしょうか。

React.jsでは、ステートの値が変更されたタイミングで、そのステートを使用している箇所のみを再描画することができます。

これにより、インタラクティブなUIの作成が可能です。

どういうことか、実際に試してみましょう。

AnswerPanel.jsを以下のように修正します。

import React, { useState } from 'react';

export default function AnswerPanel() {
  const [inputNum, setInputNum] = useState('');
 
  const handleInputNumChange = e => {
    setInputNum(e.target.value);
  }
  
  return (
    <div>
      <p>{inputNum}</p>
      <input type="text" name="inputNum" value={inputNum} placeholder="数値を入力してください"
        onChange={handleInputNumChange} />
      <button type="button">回答</button>
    </div>
  );
}

inputタグの前に、pタグでステートinputNumの値を出力する処理を追加しただけです。

これで再度ビルドして実行してみましょう。

$ yarn run build

画像5

テキストボックスに入力した値が、テキストボックスの上に出力されるかと思います。このように、ステートの値を更新したタイミングで、そのステートを使用している箇所を再描画できることが、ステートを使うメリットの1つになります。

これで、ステートの基本的な使い方は以上です。

追加したpタグはもう不要なので、削除しておきましょう。

propsについて

React.jsでは、ステートの他に、propsも重要な機能になります。

これは、親コンポーネントから子のコンポーネントに対して、値(関数を含む)を渡すことができる機能です。

どういうことか見ていきましょう。

現在作成しているコンポーネントでは、以下のイメージの構成になっています。

<App>
  <AnswerPanel />
  <ResultPanel />
</App>

このとき、Appコンポーネントが親コンポーネント、AnswerPanelコンポーネントとResultPanelコンポーネントが、Appコンポーネントの子コンポーネントになります。

こうした場合に、親コンポーネントであるAppコンポーネントから、子コンポーネントであるAnswerPanelコンポーネントやResultPanelコンポーネントに値を渡すことができるのが、propsの機能になります。

実際に試してみましょう。

App.jsを以下のように修正します。

import React from 'react';
import AnswerPanel from './AnswerPanel';
import ResultPanel from './ResultPanel';

export default function App() {
  return (
    <div>
      <AnswerPanel num1={123} num2={456} />
      <ResultPanel />
    </div>
  );
}

AnswerPanelに、属性num1とnum2を追加し、値としてそれぞれ、数値の123と456を設定しています。

親コンポーネントから子コンポーネントにpropsを渡す際は、子コンポーネントのタグに、属性として渡したいpropsを設定します。propsは複数設定可能です。

次に、子コンポーネントであるAnswerPanel.jsを以下のように修正します。

import React, { useState } from 'react';

export default function AnswerPanel(props) {
  const [inputNum, setInputNum] = useState('');
  
  const handleInputNumChange = e => {
    setInputNum(e.target.value);
  }
  
  return (
    <div>
      <p>親コンポーネントから渡された値</p>
      <p>{props.num1}</p>
      <p>{props.num2}</p>
      <input type="text" name="inputNum" value={inputNum} placeholder="数値を入力してください"
        onChange={handleInputNumChange} />
      <button type="button">回答</button>
    </div>
  );
}

親コンポーネントから渡されたpropsを使用するには、関数AnswerPanelの引数にpropsを記載します。

あとはこのpropsを使用して、渡された値を参照することができます。

参照するには、「props.名前」で参照できます。inputタグの上に、pタグでpropsの値を出力するようにしています。

この状態で、ビルドして実行してみましょう。

$ yarn run build

画像6

propsで渡された値が参照できていることが分かります。

これがpropsの機能です。propsは、親コンポーネントから子コンポーネントに値を渡すのに使われ、子コンポーネント内で値の更新は行いません。

子コンポーネント内で更新する必要のある値を保持する際は、ステートに持たせるか、内部の変数として持たせます。

それではステートとpropsの使い方が分かったところで、ヌメロンの実装の続きを行っていきましょう。

ヌメロンの実装の続き

実装する処理の流れとしては、以下のようにします。

1. 親コンポーネントであるAppコンポーネントが初期描画されたタイミングで、正解となる3桁の値を生成
2. AnswerPanelコンポーネントでは、テキストボックスに入力された回答をステートに保持
3. 回答ボタンが押下されたら、ステートに保持している入力値を親コンポーネントであるAppコンポーネントに渡す
4. Appコンポーネントで、入力値と正解の値を比較検証し、EAT数、BITE数を算出
5. 算出したEAT数、BITE数および入力値を、子コンポーネントであるResultPanelコンポーネントにpropsとして渡す
6. ResultPanelコンポーネントでは、propsで渡された値を画面に描画

入力値(inputNum)を一旦AnswerPanelコンポーネントから親コンポーネントであるAppコンポーネントに渡して、AppコンポーネントからResultPanelコンポーネントに渡しているのは、子コンポーネント同士であるAnswerPanelコンポーネントからResultPanelコンポーネントに直接値を渡す術が無い(※)ためです。

※Reduxと呼ばれるパッケージを使用すれば実現可能ですが、今回は扱いません。

それでは順に実装していきます。

Step.1 Appコンポーネント初期描画時に正解となる3桁を生成

App.jsを以下のように修正します。

import React, { useState, useEffect } from 'react';
import AnswerPanel from './AnswerPanel';
import ResultPanel from './ResultPanel';

export default function App() {
  const [ansNum, setAnsNum] = useState([]);
  
  useEffect(() => {
    const ans = new Array(3);
    ans[0] = Math.floor(Math.random() * 10);
    do {
      ans[1] = Math.floor(Math.random() * 10);
    } while (ans[0] === ans[1]);
    do {
      ans[2] = Math.floor(Math.random() * 10);
    } while (ans[0] === ans[2] || ans[1] === ans[2]);
    
    setAnsNum(ans);
  }, []);
  
  return (
    <div>
      <AnswerPanel />
      <ResultPanel />
    </div>
  );
}

Appコンポーネント内で、正解となる3桁の数値を保持したいため、ansNumという名前でステートを追加しています。後続の処理で初期値を生成するため、この時点では、空配列を初期値にしています。

次に、useEffectという関数を呼び出します。

これは、React.jsで提供されている副作用フックと呼ばれる機能になります。副作用フックの機能では、コンポーネントの再描画が必要となるタイミング(ステートの変更)ごとに、useEffectの第一引数に指定した関数を実行することができます。

今回は、Appコンポーネントが描画されたタイミングで正解となる3桁の数値を生成(※)したいため、描画されたタイミングで実行されるuseEffectの第一引数に、正解の生成処理を実装しています。

※3桁の生成処理については、コマンドプロンプト上で動くヌメロンを実装した前回の記事を参照願います。

ただし注意点があります。

useEffectは、再描画が必要となるタイミング(ステートの変更)ごとに毎回実行されるため、第一引数を指定しただけだと、常に正解の生成処理が呼び出され続けます。そこで、第二引数を指定します。

useEffectの第二引数には配列を指定でき、配列として、変数を渡します。第二引数を設定することで、ここで渡した変数の値が変更されたときのみ、第一引数の処理を実行するように制御できます。

今回は、空の配列を渡しています。これは、どの変数が変更されても第一引数の処理を実行しないということで、初期描画時のみ第一引数を実行することが可能になります。

また、ステートと副作用フックを使用するために、useStateとuseEffectをimportに追加しています。

Step.2 回答ボタン押下時の処理を実装

回答ボタンを押下したタイミングで、テキストボックスに入力された値をAnswerPanelコンポーネントからAppコンポーネントに渡し、Appコンポーネント内で、正解の値と比較検証します。

まず、App.jsを以下のように修正します。

import React, { useState, useEffect } from 'react';
import AnswerPanel from './AnswerPanel';
import ResultPanel from './ResultPanel';

export default function App() {
  const [ansNum, setAnsNum] = useState([]);
  const [eat, setEat] = useState(0);
  const [bite, setBite] = useState(0);
  const [inputNum, setInputNum] = useState('');
  
  useEffect(() => {
    const ans = new Array(3);
    ans[0] = Math.floor(Math.random() * 10);
    do {
      ans[1] = Math.floor(Math.random() * 10);
    } while (ans[0] === ans[1]);
    do {
      ans[2] = Math.floor(Math.random() * 10);
    } while (ans[0] === ans[2] || ans[1] === ans[2]);
    
    setAnsNum(ans);
  }, []);
  
  const checkAnswer = val => {
    let eatCnt = 0;
    let biteCnt = 0;
    for (let i = 0; i < val.length; i++) {
      for (let j = 0; j < ansNum.length; j++) {
        if (val[i] == ansNum[j]) {
          if (i === j) {
            eatCnt = eatCnt+1;
          } else {
            biteCnt = biteCnt+1;
          }
        }
      }
    }
    setInputNum(val);
    setEat(eatCnt);
    setBite(biteCnt);
  }
  
  return (
    <div>
      <AnswerPanel handleOnAnswerClick={checkAnswer} />
      <ResultPanel />
    </div>
  );
}

EAT数とBITE数を格納するステートを追加し、初期値は0にしています。

入力値を格納するステートinputNumも追加し、初期値は空文字にしています。

また、入力値と正解を比較検証(※)するための関数checkAnswerを追加しています。

※比較検証の処理については、コマンドプロンプト上で動くヌメロンを実装した前回の記事を参照願います。

関数の最後で、算出したEAT数とBITE数、入力値をステートに設定しています。

関数checkAnswerは、回答ボタンが押下されたタイミングで呼び出すので、AnswerPanelコンポーネントのpropsとして、関数自体を渡しています(propsでは関数も子コンポーネントに渡すことができます)。

次に、回答ボタンが押下された際に、関数checkAnswerを呼び出す処理を実装します。

AnswerPanel.jsを以下のように修正します。

import React, { useState } from 'react';

export default function AnswerPanel(props) {
  const [inputNum, setInputNum] = useState('');
  
  const handleInputNumChange = e => {
    setInputNum(e.target.value);
  }
  
  return (
    <div>
      <input type="text" name="inputNum" value={inputNum} placeholder="数値を入力してください"
        onChange={handleInputNumChange} />
      <button type="button" onClick={() => props.handleOnAnswerClick(inputNum)}>回答</button>
    </div>
  );
}

変更したのはbuttonタグの箇所で、onClick属性を追加しています。

onClick属性には、ボタンが押下された際の処理を記載できます。

今回は、propsで渡された関数を呼び出すようにしています。

また、関数の引数として、inputNumの値を渡すようにしています。

これで、親コンポーネントで定義した関数checkAnswerに、変数inputNumの値を渡して呼び出すことができます。

これで、回答ボタンを押下して、テキストボックスに入力した値と正解を比較し、EAT数とBITE数を算出できるようになりました。

最後に、EAT数とBITE数、前回の入力値をResultPanelコンポーネントで出力するようにしてみましょう。

Step.3 EAT数、BITE数、前回の入力値を出力

App.jsを以下のように修正します。

import React, { useState, useEffect } from 'react';
import AnswerPanel from './AnswerPanel';
import ResultPanel from './ResultPanel';

export default function App() {
  const [ansNum, setAnsNum] = useState([]);
  const [eat, setEat] = useState(0);
  const [bite, setBite] = useState(0);
  const [inputNum, setInputNum] = useState('');
  
  useEffect(() => {
    const ans = new Array(3);
    ans[0] = Math.floor(Math.random() * 10);
    do {
      ans[1] = Math.floor(Math.random() * 10);
    } while (ans[0] === ans[1]);
    do {
      ans[2] = Math.floor(Math.random() * 10);
    } while (ans[0] === ans[2] || ans[1] === ans[2]);
    
    setAnsNum(ans);
  }, []);
  
  const checkAnswer = val => {
    let eatCnt = 0;
    let biteCnt = 0;
    for (let i = 0; i < val.length; i++) {
      for (let j = 0; j < ansNum.length; j++) {
        if (val[i] == ansNum[j]) {
          if (i === j) {
            eatCnt = eatCnt+1;
          } else {
            biteCnt = biteCnt+1;
          }
        }
      }
    }
    setInputNum(val);
    setEat(eatCnt);
    setBite(biteCnt);
  }
  
  return (
    <div>
      <AnswerPanel handleOnAnswerClick={checkAnswer} />
      <ResultPanel eat={eat} bite={bite} inputNum={inputNum} />
    </div>
  );
}

変更したのはResultPanelの箇所で、propsとしてEAT数、BITE数、前回の入力値を渡すようにしています。

次に、ResultPanel.jsを以下のように修正します。

import React from 'react';

export default function ResultPanel(props) {
  return (
    <div>
      {props.inputNum} {props.eat}EAT {props.bite}BITE
    </div>
  );
}

propsで渡された値を画面に出力しているだけですね。

ついでに、EAT数が3(正解)の場合に、CLEAR!!!と出力するようにしてみましょう。

JSX内には、中括弧{}を用いてJavaScriptの式を記載することもできます。

import React from 'react';

export default function ResultPanel(props) {
  return (
    <div>
      {props.inputNum} {props.eat}EAT {props.bite}BITE
      <p>{props.eat === 3 ? 'CLEAR!!!' : null}</p>
    </div>
  );
}

props.eatが3の場合は'CLEAR!!!'の文字列を、そうでない場合はnull(何も出力しない)を出力するようにしています。

この状態で、再度ビルドして実行してみましょう。

$ yarn run build

画像7

正解すると、以下のようにCLEAR!!!と出力されます。

画像8

これで、一通りヌメロンの基本的な部分は実装できたかと思います。

お疲れさまでした。

まとめ

今回、ヌメロンを題材にして、以下について簡単に見てきました。

・トランスパイラとしてbabelの使い方
・バンドラとしてwebpackの使い方
・React.jsでのコンポーネントの作り方
・React.jsでのステート、propsの使い方
・React.jsでの副作用フックの使い方

本連載では、それぞれ基本的な部分しか取り扱っていませんが、まずは各イメージが身について頂ければ幸いです。

作成したヌメロンについても、改良点として、

・入力値のバリデーション処理の追加
・過去の入力値の履歴表示
・対人とのオンライン対戦

なども実装すると勉強になるかも知れないですね。

おわり!

この記事が気に入ったらサポートをしてみませんか?