2020.05.05

React入門チュートリアル (4) フォームとイベントハンドリング


この連載記事は、これから React を学びたい JavaScript 開発者のための入門コンテンツです。対象とする React のバージョンは執筆時点で最新の v16.13 です。連載記事は以下の通り。

1️⃣ Reactとは何か
2️⃣ JSX
3️⃣ 属性と状態
4️⃣ フォームとイベントハンドリング
5️⃣ ToDoアプリを作ってみよう
6️⃣ 副作用
7️⃣ カスタムフック
8️⃣ Reactプロジェクトを始める方法

⚠ 理解を助けることを意図としているため、網羅的、リファレンス的な解説はしていません。ドキュメントを併読すると、さらに理解が深まると思います。

本章では、React におけるイベント処理について説明します。と言っても、新しい概念や機能は登場しません。前章で学んだ props と state を応用してイベント処理を実装します。

フォーム

まずは、ネイティブ(既存の HTML 要素に備わっている)イベント処理の代表として、フォームの扱いについて学びましょう。

テキスト

フォーム入力は典型的に、入力値を保管する state と、onChange で state を更新するイベントハンドラを組み合わせて実装します。

function App() {
  const [name, setName] = React.useState('John');

  const handleChange = e => setName(e.target.value);

  return (
    <>
      <h1>Hello, {name}</h1>
      <input value={name} onChange={handleChange} />
    </>
  );
}

const root = document.getElementById('root');
ReactDOM.render(<App />, root);

出力結果を確認しましょう。入力値が更新されると、表示も同時に更新されます。

See the Pen React Tutorial Example 4.1 by Masahiro Harada (@MasahiroHarada) on CodePen.

textareainput と同様です。HTML 要素とは記述が異なるので注意しましょう。

<textarea value={val} onChange={handleChange} />

React は機能が絞られたライブラリです。イベント処理やフォーム処理のための特別な構文や機能は用意されていません。onChange に渡されるハンドラの第一引数も、普通の Event オブジェクトのラッパなので、たとえば inputtypefile の場合は、e.target.files でアップロードされたファイルを取得することになります。

ラジオボタン

ラジオボタンの場合もほぼ同様ですが、checked の値を明示的に判定する必要があります。

function App() {
  const [val, setVal] = React.useState('cat');

  const handleChange = e => setVal(e.target.value);

  return (
    <>
      <label>
        <input
          type="radio"
          value="cat"
          onChange={handleChange}
          checked={val === 'cat'}
        />
        猫派
      </label>
      <label>
        <input
          type="radio"
          value="dog"
          onChange={handleChange}
          checked={val === 'dog'}
        />
        犬派
      </label>
      <p>選択値:{val}</p>
    </>
  );
}

See the Pen React Tutorial Example 4.2 by Masahiro Harada (@MasahiroHarada) on CodePen.

チェックボックス

チェックボックスもラジオボタンと同様です。ただし、複数選択になるので、入力値は配列で保持するのがよいでしょう。

function App() {
  const [val, setVal] = React.useState(['js']);

  const handleChange = e => {
    // change したのはいいとして、ON なのか OFF なのか判定する必要がある
    if (val.includes(e.target.value)) {
      // すでに含まれていれば OFF したと判断し、
      // イベント発行元を除いた配列を set し直す
      setVal(val.filter(item => item !== e.target.value));
    } else {
      // そうでなければ ON と判断し、
      // イベント発行元を末尾に加えた配列を set し直す
      setVal([...val, e.target.value]);
      // state は直接は編集できない
      // つまり val.push(e.target.value) はNG ❌
    }
  };

  return (
    <>
      <label>
        <input
          type="checkbox"
          value="js"
          onChange={handleChange}
          checked={val.includes('js')}
        />
        JavaScript
      </label>
      <label>
        <input
          type="checkbox"
          value="python"
          onChange={handleChange}
          checked={val.includes('python')}
        />
        Python
      </label>
      <label>
        <input
          type="checkbox"
          value="java"
          onChange={handleChange}
          checked={val.includes('java')}
        />
        Java
      </label>
      <p>選択値:{val.join(', ')}</p>
    </>
  );
}

See the Pen React Tutorial Example 4.3 by Masahiro Harada (@MasahiroHarada) on CodePen.

繰り返しになりますが、React はフォーム処理について特定の実装方法を提供しません。そのため、入力値をどのようなデータ構造で保持するかなどは、アプリの必要性に合わせて実装します。たとえば、チェックボックスは以下の方法でも実装できます。

function App() {
  const initialVal = { js: true, python: false, java: false };
  const [val, setVal] = React.useState(initialVal);

  const handleChange = e => {
    const newVal = Object.assign({}, val, {
      [e.target.value]: !val[e.target.value]
    });
    setVal(newVal);
  };

  return (
    <>
      <label>
        <input
          type="checkbox"
          value="js"
          onChange={handleChange}
          checked={val['js']}
        />
        JavaScript
      </label>
      <label>
        <input
          type="checkbox"
          value="python"
          onChange={handleChange}
          checked={val['python']}
        />
        Python
      </label>
      <label>
        <input
          type="checkbox"
          value="java"
          onChange={handleChange}
          checked={val['java']}
        />
        Java
      </label>
      <p>選択値:{Object.keys(val).filter(item => val[item]).join(', ')}</p>
    </>
  );
}

セレクトボックス

セレクトボックスの場合は、<select>value に入力値を指定します。

function App() {
  const [val, setVal] = React.useState('react');

  const handleChange = e => setVal(e.target.value);

  return (
    <>
      <select value={val} onChange={handleChange}>
        <option value="react">React</option>
        <option value="vue">Vue.js</option>
        <option value="angular">Angular</option>
      </select>
      <p>選択値:{val}</p>
    </>
  );
}

See the Pen React Tutorial Example 4.4 by Masahiro Harada (@MasahiroHarada) on CodePen.

コンポーネントのイベント処理

続いて、自作コンポーネントにおけるイベント処理について学びましょう。

props で関数を渡す実装方法はネイティブイベントと同様です。ただし、自作コンポーネントでイベントパターンを用いるには、まず「いつどこで使うか」を判断できなくてはいけません。この点に関しては、コンポーネントの親子関係の文脈で理解するとよいでしょう。

「属性と状態」の章で、子コンポーネントは親コンポーネントから渡された props を直接更新できないと説明しました。

function Parent() {
  const [val, setVal] = React.useState(0);
  return <Child value={val} />;
}

function Child({ value }) {
  value = 123; // ❌ こういうことはできない
  // ...
}

しかし、親コンポーネントで管理する state を、子コンポーネント内のボタンが押されたときや、非同期処理が終わったときなど、子コンポーネントでしか分からないタイミングで、操作したい場面があると思います。そういうときに、イベントパターンが役立ちます。

function Parent() {
  const [val, setVal] = React.useState(0);

  // 子に渡すイベントハンドラ
  // ここで、子で指定される引数(例では value)をもとに state を更新する
  const handleUpdate = value => setVal(value);

  return <Child onUpdate={handleUpdate} />;
}

function Child({ onUpdate }) {
  onUpdate(123); // ✅ 親から渡された関数を実行する
  // ...
}

以下は、タブ機能を実装したサンプルです。

function Tab({ onChange }) {
  return (
    <ul>
      <li onClick={() => onChange(1)}>React</li>
      <li onClick={() => onChange(2)}>Vue.js</li>
      <li onClick={() => onChange(3)}>Anguar</li>
    </ul>
  );
}

function App() {
  const [tab, setTab] = React.useState(1);

  const handleChange = val => setTab(val);

  return (
    <>
      <Tab onChange={handleChange} />

      <div hidden={tab !== 1}>
        A JavaScript library for building user interfaces
      </div>
      <div hidden={tab !== 2}>
        The Progressive JavaScript Framework
      </div>
      <div hidden={tab !== 3}>
        One framework. Mobile &amp; desktop.
      </div>
    </>
  );
}

const root = document.getElementById('root');
ReactDOM.render(<App />, root);

要点が伝わりやすいようにスタイルは省きました。

See the Pen React Tutorial Example 4.5 by Masahiro Harada (@MasahiroHarada) on CodePen.

子コンポーネントである Tab は、直接親コンポーネントの state を更新せず、props として渡された関数を実行します。その関数が何をするかは、親が定義します。引数として、子しか知らない情報を受け取ることもできます。

このようなイベントパターンは、親子コンポーネント間のコミュニケーション方法と捉えることもできるでしょう。

練習問題

練習問題に取り組みましょう。自分で考えてみることが大切ですが、もし解けなくても、解答を見て写し書きするだけでも理解が深まると思います。

問題 1

以下のフォームを作成しましょう。

「その他」を選ぶと、自由記入欄が表示されます。

/* ??? */ を埋めて、コードを完成させてください。
(練習のため、コピペではなく写して書きながら埋めるとよいです。)

const options = [
  { value: 'js', label: 'JavaScript' },
  { value: 'py', label: 'Python' },
  { value: 'rb', label: 'Ruby' },
  { value: '', label: 'その他' },
];

function App() {
  /* ??? */

  const getAnswer = () => {
    /* ??? */
    // その他が選択されている場合は、自由記入欄の入力値を返す
    // それ以外の場合は、options 配列の該当する要素の label を返す
  };

  return (/* ??? */);
}

const root = document.getElementById('root');
ReactDOM.render(<App />, root);

🤫 ヒント

  1. ラジオボタンは、options を元にループで生成しましょう。
  2. ラジオボタンとテキストそれぞれを管理する state を作成します。
  3. 回答の表示は、getAnswer 関数を呼び出します。
  4. 条件付きの表示は、&& 演算子を用いて以下のように実装します。
{num > 123 && (
  <Parent>
    <Child />
  </Parent>
)}

🙌 解答例

こちらの CodePen を参照してください。

問題 2

パスワード入力コンポーネントを作成します。

「見る(隠す)」ボタンで、入力値の表示を切り替えられます。

/* ??? */ を埋めて、コードを完成させてください。
(練習のため、コピペではなく写して書きながら埋めるとよいです。)

function Password(/* ??? */) {
  /* ??? */
}

function App() {
  const [val, setVal] = React.useState('');

  const handleChange = e => setVal(e.target.value);

  return (
    <>
      <p>パスワード</p>
      <Password value={val} onChange={handleChange} />
      <p>{val.length}文字</p>
    </>
  );
}

const root = document.getElementById('root');
ReactDOM.render(<App />, root);

🤫 ヒント

  1. 入力値は親子どちらの state で管理されるでしょうか。二重管理する必要はありません。
  2. 表示の切り替えは、<input>type 属性値を切り替えて実現します。現在の type 値は、Password の状態として管理しましょう。

🙌 解答例

こちらの CodePen を参照してください。

問題 3

数あてゲームを作成しましょう。

ルールは以下の通りです。

  • 1〜50までのランダムな正解数値が割り当てられる。
  • 入力欄に数字を入れて「予想する」ボタンをクリックすると、正解か、もしくは予想が正解より大きいか小さいかが表示される。
  • 5回までの予想でに正解できないと終了のメッセージが表示される。
  • 「はじめから」ボタンをクリックすると、正解数値、残り予想回数、メッセージ表示がそれぞれリセットされる。

/* ??? */ を埋めて、コードを完成させてください。
(練習のため、コピペではなく写して書きながら埋めるとよいです。)

const random = (max) => {
  return Math.floor(Math.random() * Math.floor(max)) + 1;
};

function Guess(/* ??? */) {
  /* ??? */
}

function NumberGuessing() {
  const max = 50;
  const initialCount = 5;
  const [answer, setAnswer] = React.useState(random(max));
  const [count, setCount] = React.useState(initialCount);
  const [message, setMessage] = React.useState('');

  const judge = num => {
    if (count === 0) return;

    setCount(count - 1);

    if (num === answer) {
      setMessage('正解です!');
    } else if (count === 1) {
      setMessage('残念でした! 正解は' + answer);
    } else if (num > answer) {
      setMessage('もっと小さいです');
    } else if (num < answer) {
      setMessage('もっと大きいです');
    }
  };

  const replay = () => {
    setAnswer(random(max));
    setCount(initialCount);
    setMessage('');
  };

  return (/* ??? */);
}

const root = document.getElementById('root');
ReactDOM.render(<NumberGuessing />, root);

🤫 ヒント

  1. 入力欄と予想ボタンを Guess コンポーネントとします。
  2. 入力値は Guess コンポーネント内で管理することにします。
  3. 正誤判定(judge)は親コンポーネントの役割にします。

🦄 コード解説

以下の state の更新タイミングは注意が必要です。

setCount(count - 1);
// 中略...
} else if (count === 1) {
  setMessage('残念でした! 正解は' + answer);

setCount を実行している行で残回数をカウントダウンしているので、その下のゲームオーバー判定行では残りゼロかどうかを見ればいいようにも思えますが、そうではありません。

state 更新による再レンダリングは、処理がすべて終わってから実行されます。state 更新関数が呼ばれた直後に実行されて、そのあと次の行に戻ってくるわけではないです。

state を参照するための変数も、更新関数呼び出し直後に参照しても更新はされていません。再レンダリング後に参照して初めて更新されています。

というわけで、ゲームオーバー判定行では残回数 count の値はまだ引き算される前なので、ゼロではなくその一つ前で判定しています。

最初は間違えやすいポイントだと思いますが、上で説明した state の更新サイクルを把握すれば対応できるでしょう。

🙌 解答例

こちらの CodePen を参照してください。

連載

  1. Reactとは何か
  2. JSX
  3. 属性と状態
  4. フォームとイベントハンドリング
  5. ToDoアプリを作ってみよう
  6. 副作用
  7. カスタムフック
  8. Reactプロジェクトを始める方法