メインコンテンツまでスキップ

Reactアプリケーション向けUIコンポーネントライブラリを比較検討する

Sato Taichi
yak shaver

僕の利用形態としては、React Aria + Tailwind が最も妥当性が高いということになった。

という話を、ここから延々と説明する。何の話をするのかわかる人や結論だけ欲しい人向けの情報は、これ以降何も存在しないので、どうぞよしなに。

はじめに

久しぶりにフロントエンド仕事をしようと思って、手元のテンプレートリポジトリを見たら、何もかもが時代遅れになっており大変につらい気持ちになったのがつい先月のことだ。

やれやれだが全部を選びなおすってのも、まぁ悪くないよなって考えた自分がいかに甘かったか、このエントリでは出来る限り説明していく。

なんのためかって?未来の自分のためだ。将来の自分から見て現在の自分がどのくらいの能力だったのかというのを、きちんと記録しておくことには重要な意味がある。

そして、この記録を有識者の皆さんに御覧頂くことで、何か有益な指摘をもらえるかもしれない、という微かな希望もある。

議論の範囲と要件

まずフロントエンドアーキテクチャ全体を少ない時間で議論するのはもう無理だ。知識範囲が広くなりすぎてて全く追い切れる気がしない。アプリケーションが動作するためには、サーバサイドだってクラウドインフラだってあるのに、ユーザに接するフロントエンドにかけられる時間は、それほど多くはない。というわけで、今回はUIコンポーネントライブラリについてのみ議論することとする。それだって広すぎる。

また、基盤となるライブラリはReactのみとする。AngularやVue3、Svelteあたりは良いものだと思うが、いかんせん僕が知らなすぎる。そして、そのレイヤーからやり直すのはつらい。よってReactに特化したAPI構造をしていることは必須だ。色んなフレームワークに対応するための抽象化レイヤーなんてものは百害あって一利なし。

次に、CSSは自由にあてたいし、DOMの構造はいじりたい。要はスタイルをちょっと変な弄り方したいだとか、コンポーネントの一部にちょっとした機能追加したいだとか、そういうことのためにコンポーネントを丸っとコピペして作り直すみたいな事はやりたくないってこと。

そして、モバイル対応はしなくてもいいけども、ウェブアクセシビリティは確保したい。言い換えるとWAI-ARIAへの対応をできる限り少ない工数で実現したい。世の中に出回ってるスクリーンリーダーで読み上げして貰い易いようなDOMを作ってくれると良いんだけども、そんなことは可能なんだろうかね。

何でこんなことを言い出してるかって?そろそろ老眼がきつくなってきて、目が自在に見えるって事が当たり前じゃなくなってきてるんでね。僕が抱える体調不良の大きな原因の一つは眼精疲労によるものだ。音声だけでアプリケーションを操作できたら目を休ませられるかもしれない。それで、頭痛や肩こりを少しでも低減したい。その延長で色の違いが少々分からんかったり、視野が欠けても使えるようなアプリケーションが増えたらいいよね。

キーボードやマウスだけでなくタッチパネルみたいなデバイスに対応する際にもガイドになるような機能が用意されていると加点だね。今はまだキーボードやマウスを使えているけども、段々怪しくなってきてるんだよね。

評価対象となる要素技術の洗い出し

Reactを前提に、CSSを自由にあてたいって時点でヘッドレスコンポーネントライブラリを選択することになる。その上でWAI-ARIAに対応するってなると選択肢は大きく絞り込まれる。

具体的には、この3つしか見つけられなかった。

  • Radix Primitives
    • Radix UIというスタイリング済コンポーネント集が存在している。それを実現するためのスタイリングされていないコンポーネントライブラリが独立して使えるようになっているもの。
    • スタイリング済のコンポーネントが使いたい場合は、Radix Themesを使う。
  • React Aria + React Stately
    • Adobeが定義したSpectrumというデザインシステムをReactで実装したものがReact Spectrum だ。これには4つのライブラリが含まれている。
      • React Spectrum
      • React Aria
      • React Stately
      • Internationalized
    • React Aria と React Stately はデザインシステムとは独立して利用できるHooksのライブラリだ。
    • React Ariaの中にはそのまま使えるコンポーネントライブラリとして React Aria Componentsが含まれている。
  • ariakit
    • コンパクトなライブラリでコンポーネントの状態管理用HookはAPIとして分離されている。
    • OSS版はコンポーネント数が少ないように見えるが上位の有償版が存在する。

この手のライブラリを選ぶときには、外側にあるエコシステムを無視することはできない。

例えば、最近人気のツールとして shadcn/ui がある。こいつは、RadixとTailwindで作ったサンプルコードを自分のプロジェクト内にコピペしてくれるのだ。

Radixのドキュメントは、よく出来ていてTailwindでスタイリングしたコンポーネントのサンプルコードをパッとコピペできるようになっている。それを効率化しつつ、新しいコンポーネントを追加したり、AIでUIコンポーネントを作れる v0 と連携できるようにしたものが shadcn/ui だ。

大変に興味深い事に shadcn/ui が提供するツールでReact AriaとTailwindを基盤にしたコンポーネントをコピペすることもできる。それが、JollyUI だ。

React AriaとTailwindを基盤にしたコンポーネントライブラリとしては、Hero UI もある。React AriaのCopomnent APIよりも更にパッケージングされているので、自由度は下がるがさっさと作れる。OSS版はコンポーネント数が少ないけども上位の有償版が存在する。網羅性を確保するために紹介したが、柔軟性が相対的に低い事が明らかなので、今回は検討しない。

何の細工もなしにCSSを書くってのは大変につらい。というわけでCSSのユーティリティとして、Tailwind CSS を検討する必要があるだろう。他にもCSS フレームワークが沢山ある事は何となく知っているが追いきれていない。

ここまでに出てきた要素技術を図示するとこうだ。

ライブラリ相関図

様々な課題設定があり、それぞれに対応している範囲が違う。選定基準を明確にしなければ、どれを選んでも後悔することになるだろう。

要素技術の詳細な検討

ここからは、列挙した要素技術について詳細に検討していこう。

デザインシステムについて

まず明確にしておかないといけないのは、僕はデザイナではないし、その素養もない。よってデザインシステムそのものの是非についての意見はない。

今回の選定対象の中に含まれているデザインシステムの実装は Radix ThemesとReact SpectrumとTailwind CSSだ。Tailwind CSSだけがちょっと特殊でデザインシステムを作るためのフレームワークにデフォルトのデザインシステムが最低限付いてる、みたいな理解をしている。

作られているドキュメントの内容や質という意味では、React Spectrumが優れているようには感じる。しかし、Radix Themesで提供されているドキュメントがあればアプリケーションを作るうえでは十分だ。

この数か月対話している生成AIの皆さんが上手く扱えるのはどうやらTailwindっぽいという、あまり根拠のない肌感覚みたいなものはある。生のままのCSSは、どうも思い通りに作ってくれないのよね。僕が持つ語彙が足りない可能性は大いにあるだろう。

というわけで、デザインシステムの選定を棚上げしたい自分としてはTailwindを使っておこうという判断になる。

後は一緒に仕事をすることになるデザイナーさんがTailwind慣れしていてくれることを祈るばかりだ。

ヘッドレスコンポーネントライブラリの比較

3つのライブラリを比較検討するにあたって利用できるコンポーネントの数は、まず比較対象になるだろう。使わないものが沢山あってもしょうがないが、使いたいものが無いなら作るしかない以上、基本的には多い方がいい。

  • Radix Primitives は28個
  • React Aria は45個
  • ariakit は17個

Radix Primitivesの特徴は何と言ってもasChild だろう。このプロパティを有効にすることで、子要素として指定したコンポーネントが代わりにレンダリングされる。これによって、可読性を維持したままコンポーネントの振る舞いを大きく切り替えられる。型のサポートもばっちりだ。詳細は公式のドキュメントを確認して欲しい。cf. Composition

各コンポーネントの状態管理は、コンポーネントの内部にカプセル化されており、調整の余地はあまりない。これがRadixの分かり易さであり、融通の利かない部分でもある。

既存コンポーネントのDOM構造を弄りたくなっても、それはできないのでasChild を使って置き換えるという事になる。

asChild は、大変にクレバー仕組みだが、僕には動作が難しすぎると感じる。

React Ariaの特徴はガチムチなHookベースAPIだろう。何に難しさを感じるかは人によるだろうが、Reactを正しく理解しているなら、React Ariaは正しく理解できるはずだ。React Hooksがそもそも難しすぎるだろうという指摘については同意する。

UIコンポーネントとしての振る舞いを定義したHooksをパッケージングしてあるのがReact Ariaである。UIコンポーネントの状態を管理するHooksをパッケージングしてあるのがReact Statelyだ。

問題なのは、Hookだけそうやって提供されたからって画面には何も表示されないってことだ。それらをJSXに適用するのはライブラリの利用者側の責任ってわけだ。それはつまり、ボイラープレートコードが爆増することが予想される。そんな分かりやすい問題がほったらかされてる訳もなく、React Aria Componentsというライブラリが提供されている。これで最低限DOMとHookが統合されたUIコンポーネントが使える。

ariakitは有償のコンポーネントがあるのであまりフェアな比較ではないけども、買ったからといって、それほど大きな差が発生するようには思えない。利用する状況が限定されている複雑なコンポーネントが多くでも使いようがないわけだし。金を払う前にどんなコンポーネントがあるのか確認できなかったので評価し辛い。

それほど大きな話ではないけども、ariakitはServer Side Renderingに対応しているかどうかについて言及が無かったのも残念。この時点でariakitを選択することは無くなった。

というわけで、基本的にはReact Aria Componentsででき合いのコンポーネントを使いつつ、必要な場所ではHook APIを組み合わせて、より状況に強く依存したコンポーネントを作っていくのが良いだろうと結論した。

React の Hooksについては、随分前に時間をかけて学習したので新しく覚えることは少ない。RadixのasChild が、どういう動作なのかは、少し調べてみたが正確には分からなかった。能力不足だと言われれば、残念だが否定しようがない。

コピペツールの是非について

shadcn/ui は大変に良いツールだ。UIコンポーネントライブラリが持つ根本的な課題を解決している。それは何かというとカスタマイズ性だ。こに対処するための根本的解決手段として、UIコンポーネントを提供するのではなく、コピペを効率よく実施するための手段を提供する。これは、かなりクレバーなアプローチと言える。

どうせカスタマイズのために、元のコードベースをコピペして必要な場所を変えて動かすことになるんだから、最初っからそうすればいいってこと。

これによって、画面の構成部品を作るという事に対する生産性は明確に向上する。コマンド一発で各コンポーネントがローカルディレクトリにバンバンコピペされるんだから、時間当たりの生産されるコード行数は目に見えて増える。なんてこったい、これはマジで最高だな。

だが、ちょっと待ってほしい。5個10個とコンポーネントをローカルにコピペしてるうちに気が付くことがある。これらは、どうやってメンテナンスするんだろか?見た目良い感じに動いてはいる。後の事は、後の人が何とかするっしょ。基盤部分はRadixなんだから最低限の品質は担保されてるさ。起きてもいない問題を解決しようとするべきでない。そういう考え方ができるならOK。v0でコンポーネントを作りまくろう。

残念ながら、僕はそういう風には考えない。基本的な振る舞いを確認するためのユニットテストすら付いてこないものを、正しく動作しているって自信を持って言えるほどの蛮勇はどっかに置いてきてしまった。これが老いってやつだ。細かいことが気になってしょうがない。

基盤はRadixだっていうけど、なんか気が付いたらTanStack Tableが依存性に入ってる、いつ入ったんだ?おいおい、話が違うじゃねぇか。いや、TanStack Tableは良いライブラリだよ。そこに疑問は無い。でもよ、知らんうちに入り込んでるのは違うんじゃねぇかな。これからも知らんものが知らうちに入ってくるのか?ローカルにコードがあるんだから、それは利用者側の自己責任ですってわけだ。勘弁してくれ。

コンポネントに何か脆弱性があっても、それに一々対処するには、ライブラリのアップデートじゃ済まなくて、自己責任でコピペしたものを逐一全部確認して回る必要があるってことね。きつすぎる。

コピペされてくるコンポーネントがpath aliasを前提にimportしてんのも大分気にいらない。こいつがいるだけで、開発環境のセットアップがとてつもなく複雑になる。import文の煩雑さをちょっと低減するというリターンに対して、path aliasがもたらすリスクは許容限界を超えてると個人的には感じる。

JavaScript界隈のモジュール名を解決するロジックは歴史的経緯によってアホほど複雑だ。tscみたいなコンパイラとESLintみたいなLinterとwebpackみたいなモジュールバンドラが、整合性をもってモジュール名を解決できるようプロジェクトを構成するには、とんでもない慎重さが求められる。

なんかよく分からんプラグインをあっちこっちに入れたり、設定を書いたり、処理順を正しくしたりする。一度動いてしまえば、何度でも動くようになるけども、すぐに誰も触れない禁断の領域になる。そして、使ってるツールをアップデートするとよく分からんプラグインが不要になったり、設定のデフォルト値が変わったりする。プロジェクトが成長してきたのでディレクトリ構成を整理しなおしたいとか思っても、その魔界は許してくれないだろう。

というわけで、コピペツールの導入は見送り。

まとめ

最近のUIコンポーネントライブラリってのは、すげぇもんだね。どれもこれも立派に動く。色んな事を詳細に考えてきちんと作りこまれている。

僕が認識していないUIに関する課題ってのは、まだまだ沢山あるだろうし、それを解決するために作られたライブラリはもっと沢山あるだろう。

このエントリ内に何か見当違いな事を書いている可能性については特に否定しないので、気が付いた人はXか何かにシュッと書いてくれると非常にありがたい。テキトーにエントリのURL何かを付けといてくれれば、こっちからエゴサしとくんでよろしくお願いしますね。

この長文に付き合ってくれるような忍耐力のあるあなたに感謝をささげて、エントリの〆とします。