JavaScriptアーキテクトのJed Watson氏が、先日のReact Conf 2019で講演して、多数のユースケースへの対応によって必然的に拡大するコンポーネントが引き起こす、設計およびメンテナンス性の問題へのソリューションを提案した。Watson氏が論じたのは、npmで毎週250万ダウンロードを数え、ルックアンドフィールをカスタマイズする100以上のオプションを持つ、react-selectコンポーネントのケースである。
Watson氏は2014年10月、限定的な機能を持ったreact-selectを開発した。その後5年間、プロジェクトがGitHubスター1個から18,000以上にまで成長し、ダウンロード数が毎週250万を数えるようになるまでになったreact-selectは、search-as-you-type、single-selectとmulti-select、フォーカス管理、メニューの多層化と位置指定、非同期メニュー項目、アクセシビリティ、キーボードとタッチ操作のサポート、生成可能なアイテムなど、さまざまな機能を提案した。機能の拡張は、それまでよりも多様な状況のユーザがコンポーネントをダウンロードして利用することにより、さらに大きなユースケース提供への要求へと、直接的に結びついた。
それによってreact-selectはスコープを広げ、複雑で入り組んだロジックを持つに至ったのだ。Watson氏は、標準的な分析アプローチでは制限された結果しか得られない、という事実に取り組む必要に迫られた。氏は次のように説明する。
新たなユースケースに対応するには、毎回、コアコンポーネントをさらに複雑にする必要がありました。[…]このようなさまざまなユースケースに必要なカスタマイズは5パーセント程度に過ぎなかったのですが、その5パーセントが一人ひとり違っていたのです。そのため、カスタマイズ可能な領域が、コンポーネントの大きな部分を占めるようになりました。その結果、イシューやPRが山積し、エッジケースが他のエッジケースと干渉するようになったのです。
そこからWatson氏は、プロジェクトを継続的にメンテナンスするためには、オープンソースのコントリビュータたちがバグや干渉のないPRを容易に提供できると同時に、ユーザがいくつかのプロパティをカスタマイズしてユースケースを実装できるようにコンポーネントを設計し、コードをアーキテクトする必要がある、という認識に至った。
Watson氏がコンポーネントの関心事として取り上げたのは、状態管理、機能、ビュー、スタイルの4つである。コンポーネントのユーザは、コンポーネントインターフェースを通じて、これら4つの関心事に触れることができる。Reactの場合、これはReact propsインターフェースになる。さらに、コンポーネントユーザがハイオーダ・コンポーネントなど、既知のReactコンポジションパターンを使用する場合もある。
状態管理の問題は、特定の状態部分(piece of state)に対処する高次のコンポーネントに分解することができる。Watson氏はselectコンポーネントの重要な部分を特定して、"select from"、"input value"、"selected option"、menuIsOpen
、isLoading
のなどの"toggle"、という4つのオプションの配列にして、一般的なWebのパターンに倣って、コールバックを使用したコンポーネント動作のカスタマイズを可能にした。共通パターンは、コールバックを実装する高次コンポーネントを含んでおり、状態を部分的に反映するpropの更新を行う。Watson氏は例として、"input value"を処理して"toggle"の変更をロードする、manageState
とmakeAsync
という2つの高次コンポーネントを紹介した。これらは次のように使用する。
import BaseSelect from './Select';
import manageState from './manageState';
import makeAsync from './async';
export default manageState(Select);
export const AsyncSelect =
manageState(makeAsync(Select));
機能のカスタマイズと拡張は、"option label construction"、"option filtering"、"option loading"のように、単一責務のpropsを通じて実現されている。これは、修正に対してはクローズドだが拡張に対してはオープンなインターフェースを規定するという、open/clodesの原則に従ったものだ。"custom filtering"の例は次のようなものになる。
import Select from 'react-select';
filterFn = (candidate, input) =>
candidate.firstName.contains(input) ||
candidate.lastName.contains(input);
<Select filterOption={filterFn} />
ビューとスタイルは機能の状態だ。ビューは25のコンポーネント(components
オブジェクトpropをキーとして集められる)に分割されており、組み合わせることでreact-selectコンポーネントを表示する。25のコンポーネントには、さまざまなコンテナのビューの他、"separator"、"indicator"、"input value"、"menu list"などが含まれている。このように細分化された単一責務のコンポーネントを用意することで、コンポーネントユーザが必要なパーツを集めてカスタマイズできるようにしているのだ。単一責務の各コンポーネントは、スタイルpropにマッチさせることによって、コンポーネントユーザがコンポーネントの外見をカスタマイズできる。style
propを計算で求める方法として、氏は次のような例を提供している。
options = [
...
];
valueStyles = (styles, {data}) => ({
...styles,
backgroundColor: data.colour,
});
<Select
options={options}
styles={{ value: valueStyles }}
/>
例の中で示したように、ユーザ定義関数やコールバックをカスタマイズ目的で広範に使用することが可能になっている。指定された関数は、状態部分や既定のスタイルなど、react-selectコンポーネント内部の関連データに渡される。この構成は、高レベルのモジュールは低レベルのモジュールに依存するべきではない、いずれも抽象化されたものに依存すべきである、という、依存性逆転の原則(dependency inversion principle)を想起させる。この場合の抽象化はpropインターフェースで、カスタマイズ関数はこれを通じてreact-selectコンポーネントに注入される。有名なreact-testing-libraryオープンソースプロジェクトの開発者であるKent c. Dodds氏が初学者向けの記事で、コントロールの逆転によってコードを再利用する方法について述べている。
Watson氏はreact-selectコンポーネントのカスタマイズ能力を立証するために、propの巧みな設定によって実装した完全な日付選択機能を紹介している。アーキテクチャと設計によってこのレベルまで到達することで、オープンソースメンテナとしての自分の負荷を大きく軽減することができた、と氏は主張する。コンポーネントをカスタマイズする作業は、事実上コンポーネントユーザに託されたのだ。
react-selectコンポーネントで採用された原則はReact特有のものではなく、インターフェースや実装の詳細が変更される可能性のある他のUIフレームワークに汎化されるものだ。render関数を備えたフレームワークであれば、同じようなインターフェース分解テクニックが使用できる可能性がある。テンプレートベースのフレームワークであっても、高次コンポーネントパターン(レンダレス・コンポーネント(renderless component)と呼ばれる場合もある)を使用している場合はあるが、render propではなく、スロットが使用される。スタイリングは、CSS変数(IE11以外のほとんどのブラウザでサポートされている)あるいは/またはインラインstyleを使って処理される。あるSvelteユーザが、CSSプロパティを使った動的スタイルのサンプルを公開している。
Watson氏の講演の全内容は、他のコードスニペットや詳細な説明と合わせて、ReactConfサイトで公開されている。ReactConfは公式なFacebook Reactイベントで、2019年はネバダ州ヘンダーソンで、10月24、25日に開催された。