イベントハンドラ Props は optional にする

English version

イベントハンドラって渡されなくてもそのコンポーネントは困らないよなということで、React においてもタイトルの書き方をこれからはやっていきたいという気持ちを表明しておく。

自身へのイベントハンドラは誰のためにあるのか

あるコンポーネント自身が受け取ったイベントハンドラは、その利用者である、親となるコンポーネントのためにある。

なぜコンポーネントという概念にイベントとイベントハンドラが必要になるかというと、逆方向のデータフローを実現するためだ。「コンポーネントの state を更新できるのは自分自身だけであるべき」ということで、親からデータを貰っているときは、間接的にその更新をイベントとして表現し、コールバック(つまりイベントハンドラ)を通して親に伝え、親にデータを更新してもらう。 ref: React の流儀 - Step 5: 逆方向のデータフローを追加する

ここで、受け取ったイベントハンドラはそのコンポーネントそのものの動作に影響を与えないことに注意する。無論、画面上で組み込まれたのを見ると動作が変わっているように見えるが、これはコンポーネント自体の動作が変化したのではなく、その親の動作が変わったのだ。親が子からイベントハンドラを通してイベントを受け取り、それに従った状態変化を起こし、最終的に子に更新後のデータを渡したということだ。親はそのイベントを使っても使わなくても良いし、使う場合でも子に遠慮せず親がデータを好きに加工して子に渡しても良い。受け取ったイベントを使うかどうかを含め、更新のロジックは子でなく全て親が握っている。逆に言うと、自身が子となる関係においてて自身が受け取ったイベントハンドラがどのように使われるかを知る必要はない[1]。イベントハンドラが渡されず undefined だったとしても構わなし、動作に変わりはない。

現に、HTML 要素においてイベントハンドラは全て任意になっている。focusininput イベントを必ずしも使う必要はないし、すべてのイベントにイベントハンドラを渡すコードを(フレームワーク開発以外で)書くことは無い。そして、イベントハンドラを渡さないことで HTML 要素が困る(エラーを出す)ことはないだろう。

Vue や Angular は HTML ベースの記述法ということで、イベントハンドラという概念は自然に optional となるが、React ではイベントハンドラを Props の一部として表現する関係上、required になってしまいやすい。呼び出し時にオプショナルチェーン(?.)をつけ、TypeScript であれば Prop 名の後ろに ? を書く、ということを徹底している必要がある。

でも、必要な場面もあるのでは?

すべての「イベントハンドラ」を optional にしても、困る場合は無いと考えている。

イベントハンドラでない関数引数

イベントハンドラ Prop の型は、(...args: [...Args]) => void となる、戻り値がない関数のはずだ。場合によっては、イベントハンドラ⊂関数引数ということでそれ以外の関数、例えばデータを取得するような関数 (...args: [...Args]) => Promise<Data> 等も Props に存在するかもしれない[2]。これらは渡されないとコンポーネントの動作に支障をきたすものの、これらはイベントハンドラではないので、イベントハンドラは optional にすべきという主張とは関係が無い。

協調動作するグループ中のあるコンポーネントの場合

上でも少し触れたが、親などとイベントハンドラを通した協調動作が前提となるコンポーネントであっても、イベントハンドラを受け取ることが そのコンポーネントにとって 必須とまでは言えない(周りの役には立たないかもしれないが)。協調動作するグループにとってそのコンポーネントにイベントハンドラを渡すのが動作の実現上必要だということは言えるが、それは上述した親子関係と同じく、協調動作するグループがイベントをどう使うかという話で、その構成要素となる一つのコンポーネントそのものの問題ではない。

HTML 要素においても、例えば <button type="button" /> では(通常の利用法では)click イベント等を使わないと役に経たないが、使わなくてもこのボタン自体がエラーを出すわけではない。ただ、そのボタンを利用する親コンポーネントが要件を満たせず困るので、親はイベントハンドラを渡す。

optional にしないことによる不便なこと

最後に、イベントハンドラが optional でないとき困る細かいことを列挙しておく。

コンポーネントカタログやテストでのイベントハンドラ

コンポーネントカタログでは、コンポーネントの様々なパターンを表示する。このとき、イベントハンドラが required だと、カタログ上必要でないところまで、適当に見繕ったイベントハンドラ[3]をすべて渡す必要がある。おそらく、イベント発生を知らせるのは動作デモ用の部分だけにあれば十分で、色のバリエーション等を示すのためのカタログの部分までは必要はないだろう(基本のボタンコンポーネントの色やサイズのバリエーションの部分に毎度 onClick={action("clicked")} と書いて回るのを想像・思い出してほしい)。

同じことはコンポーネントのテストにも言える。そのテスト観点に不要なイベントハンドラまで渡す羽目になり、テストで何を確認したいかが不明確になる。また、(テストということを考慮しても)ボイラープレート的なコードが増える。

「条件によって不要」なイベントハンドラ

コンポーネントに渡す値によっては、呼ばれないと分かるイベントハンドラがあることもあろう。例えば、<button />disabled のときの click イベントだ。SPA において常に操作不要なコントロール要素が設置されている理由[4]は置いておいて、利用者にとって不要と分かり切ったイベントハンドラであっても何かしらを渡す必要があるのは良くないインターフェースの証だろう。

以前、onChange が required な <input /> 系コンポーネントを使う場所で disabled 固定にされたものがあり、そこに const noop = () => {}; とした関数を渡したのは今も残念な記憶として残っている。


  1. 要求を満たすためのインターフェースとして、例えば、名前や引数について、自身の不特定の親(=利用者)と「合意」しておく必要はある。ただ、サーバーが持つ API エンドポイントでの状況と同じく、それ以上のことを知る必要は無い。 ↩︎

  2. 多くのコンポーネントでは取得したデータを data か何かの値として受け取る方が自然だとは思うが。 ↩︎

  3. 大抵は Storybook の Actions のようなものや console.log 等ボイラープレート的なもの ↩︎

  4. 編集不能な値を可能な値と同じレベルで並べて表示するものや、“Coming Soon” でまだ押せないボタン。 ↩︎