React基礎講座 #6 イベントハンドリング

読了 8分

前回はstateとuseStateを学びながら、自然とonClickというイベントハンドラを使いました。それ自体でも動作はしましたが、Reactのイベント処理方式にはいくつか知っておくべきことがあります。今回はイベントハンドリングを本格的に扱ってみます。

Reactでイベントを処理する方法 #

Reactではイベントは、JSXの属性としてハンドラ関数を渡す方式で処理します。HTMLのonclickではなくcamelCaseのonClickを使うという点だけ違います。

src/App.jsx
function App() {
  function handleClick() {
    alert('ボタンがクリックされました!');
  }

  return <button onClick={handleClick}>クリック</button>;
}

export default App;

ここでよくやるミスが1つあります。関数を呼び出して(handleClick())はいけません、関数自体(handleClick)を渡さなければなりません。

間違った例
<button onClick={handleClick()}>クリック</button>

このように書くとコンポーネントがレンダリングされる瞬間にhandleClick()が実行されてalertが出てしまいます。さらにonClickにはhandleClickの戻り値(undefined)が登録されるので、本物のクリックには何も起きません。

正しい例
<button onClick={handleClick}>クリック</button>

関数の参照だけを渡し、呼び出しはReactがクリック時点で行います。この違いを必ず覚えておいてください。

インラインハンドラ #

簡単なハンドラはJSXの中に直接アロー関数で書くこともあります。

src/Counter.jsx
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      カウント: {count}
    </button>
  );
}

() => setCount(count + 1)はクリック時点で呼び出される無名関数です。1行の単純なハンドラはこのようにインラインで置くのが便利で、ロジックが長くなれば別の関数として切り出す方が可読性によいです。決まったルールはなく、チームや本人の好みに従います。

関数に引数を渡す #

ハンドラ関数に引数を渡さなければならないときはインラインのアロー関数で包まなければなりません。

src/App.jsx
function App() {
  function handleClick(name) {
    alert(`${name}さんこんにちは!`);
  }

  return (
    <>
      <button onClick={() => handleClick('チョルス')}>チョルスに挨拶</button>
      <button onClick={() => handleClick('ヨンヒ')}>ヨンヒに挨拶</button>
    </>
  );
}

次のように書いてはいけません。

間違った例
<button onClick={handleClick('チョルス')}>...</button>

上で見たようにこれはレンダリング即座に呼び出されてしまいます。引数を渡すには必ずアロー関数で1度包んで「クリックされたときに呼び出せ」という意味を作らなければなりません。

イベントオブジェクト #

イベントハンドラは最初の仮引数としてイベントオブジェクトを受け取ります。このオブジェクトにはどの要素でどんなイベントが起きたかについての情報が入っています。

src/InputDemo.jsx
import { useState } from 'react';

function InputDemo() {
  const [text, setText] = useState('');

  function handleChange(e) {
    setText(e.target.value);
  }

  return (
    <div>
      <input type="text" value={text} onChange={handleChange} />
      <p>入力値: {text}</p>
    </div>
  );
}

export default InputDemo;

仮引数の名前は普通eまたはeventにします。e.targetはイベントが発生したDOM要素で、e.target.valueで入力値を取り出せます。

注記
Reactのイベントオブジェクトは正確にはブラウザのネイティブイベントではなく合成イベント(SyntheticEvent)です。Reactがすべてのブラウザで同じように動作するように1度ラップしたオブジェクトです。APIはネイティブイベントとほぼ同じなので、普段は気にすることがありません。e.preventDefault()e.targete.keyのような馴染みのあるプロパティ/メソッドをそのまま使えます。

よく使うイベント #

もっともよく使うイベントハンドラをまとめると:

  • onClick — クリック
  • onChange — 入力要素(input、textarea、select)の値が変わったとき
  • onSubmit — フォームが送信されたとき
  • onKeyDown / onKeyUp — キーボードのキーが押されたり離されたとき
  • onMouseEnter / onMouseLeave — マウスが要素の上に入ったり外れたとき
  • onFocus / onBlur — フォーカスの取得/解除

それぞれのイベントはそれに合った情報をイベントオブジェクトに収めます。onChangeならe.target.valueを、onKeyDownならe.key(押したキーの名前)を見ればよいです。

src/SearchBox.jsx
function SearchBox() {
  function handleKeyDown(e) {
    if (e.key === 'Enter') {
      alert('Enterキーが押されました');
    }
  }

  return <input type="text" onKeyDown={handleKeyDown} />;
}

デフォルト動作を防ぐ #

ブラウザはあるイベントに対してデフォルトの動作を持っています。フォームが送信されるとページがリロードされ、リンクをクリックするとページが移動します。このデフォルト動作を防ぐにはイベントオブジェクトのpreventDefault()を呼び出します。

src/LoginForm.jsx
import { useState } from 'react';

function LoginForm() {
  const [email, setEmail] = useState('');

  function handleSubmit(e) {
    e.preventDefault();  // フォーム送信によるページリロードを防ぐ
    console.log('送信されたメール:', email);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <button type="submit">ログイン</button>
    </form>
  );
}

export default LoginForm;

フォームは送信ボタンを押したり入力欄でEnterを押すと自動でonSubmitが発火します。このときe.preventDefault()がないとブラウザがページをリロードしてしまうので、私たちが書いた処理ロジックが意味を失います。

イベントハンドラをpropsで渡す #

イベントハンドラもただの関数なので、propsで子コンポーネントに渡せます。このパターンは子が起こしたイベントを親が処理しなければならないときに非常によく使われます。

src/Button.jsx
function Button({ label, onClick }) {
  return (
    <button onClick={onClick} style={{ padding: '8px 16px' }}>
      {label}
    </button>
  );
}

export default Button;
src/App.jsx
import Button from './Button';

function App() {
  function handleSave() {
    alert('保存されました');
  }

  function handleCancel() {
    alert('キャンセルされました');
  }

  return (
    <>
      <Button label="保存" onClick={handleSave} />
      <Button label="キャンセル" onClick={handleCancel} />
    </>
  );
}

親はどんな動作をするか(handler)を渡し、子はいつその動作が起こるか(クリック)を知らせる構造です。ハンドラpropの名前は慣例的にonで始まるように付けます(onClickonSaveonItemSelect…)。

ハンドラ内でstateを変更する #

前回見たパターンですが、イベントハンドラ内でstateを変更するのがもっともよくあるパターンです。

src/Toggle.jsx
import { useState } from 'react';

function Toggle() {
  const [isOn, setIsOn] = useState(false);

  function handleToggle() {
    setIsOn(prev => !prev);
  }

  return (
    <div>
      <p>現在の状態: {isOn ? 'ON' : 'OFF'}</p>
      <button onClick={handleToggle}>トグル</button>
    </div>
  );
}

export default Toggle;

イベントが起こると → ハンドラが実行され → stateが更新され → 画面が再描画される。このフローがReactアプリのもっとも基本的なパターンです。

自分でやってみる #

簡単な入力フォームを作ってみます。ユーザーが名前とメッセージを入力して「追加」を押すと画面の下にメッセージが追加されるコンポーネントです(次回#7、#8でこれをさらに拡張します)。

src/MessageForm.jsxを作ります。

src/MessageForm.jsx
import { useState } from 'react';

function MessageForm() {
  const [name, setName] = useState('');
  const [message, setMessage] = useState('');
  const [lastSubmitted, setLastSubmitted] = useState(null);

  function handleSubmit(e) {
    e.preventDefault();
    if (!name || !message) return;
    setLastSubmitted({ name, message });
    setName('');
    setMessage('');
  }

  return (
    <div style={{ padding: '16px', border: '1px solid #ccc', borderRadius: '8px' }}>
      <form onSubmit={handleSubmit}>
        <div>
          <input
            type="text"
            placeholder="名前"
            value={name}
            onChange={(e) => setName(e.target.value)}
          />
        </div>
        <div style={{ marginTop: '8px' }}>
          <input
            type="text"
            placeholder="メッセージ"
            value={message}
            onChange={(e) => setMessage(e.target.value)}
          />
        </div>
        <button type="submit" style={{ marginTop: '8px' }}>追加</button>
      </form>
      {lastSubmitted && (
        <p style={{ marginTop: '12px' }}>
          最後の入力: <strong>{lastSubmitted.name}</strong>  {lastSubmitted.message}
        </p>
      )}
    </div>
  );
}

export default MessageForm;

src/App.jsxに接続します。

src/App.jsx
import MessageForm from './MessageForm';

function App() {
  return (
    <>
      <h1>メッセージフォーム</h1>
      <MessageForm />
    </>
  );
}

export default App;

名前とメッセージを入力してEnterや「追加」ボタンを押してみてください。最後に送信された値が下に表示され、入力フィールドは空になります。e.preventDefault()を抜くとフォームがリロードされて入力値が消えるのも一度実験してみてください。

ヒント
上のコードでlastSubmitted && (...)の部分が初めて見えるかもしれませんが、これは条件付きレンダリングです。値があれば見せて、なければ見せないパターンです。次の#7で詳しく扱います。

おわりに #

今回の記事ではReactのイベント処理を見てきました。要点は:

  • onClickのようにcamelCase属性でハンドラを登録する
  • 関数を呼び出さずに渡す({handleClick}であって{handleClick()}ではない)
  • 引数を渡すにはアロー関数で包む
  • ハンドラは最初の仮引数としてイベントオブジェクト(e)を受け取る
  • e.preventDefault()でブラウザのデフォルト動作を防げる
  • ハンドラもpropsで子に下ろせる(onで始まる名前)

またMessageFormの例でちらっと見た{lastSubmitted && ...}パターンがそのまま次のテーマにつながります。次の記事「React基礎講座 #7 条件付きレンダリング」では、画面の一部を状態に応じて見せたり見せなかったり、別の姿に変えたりする様々なパターンをまとめてみます。

X