◆ ウェブページの作りをコンポーネントにするときに困る部分まとめ
◆ コンポーネントと言っても WebComponents と React 系ライブラリで結構違う
◆ WebComponents なら React みたいに細かくコンポーネント化しないほうがいいのかも
  ◆ コンポーネントならではのデメリットも多くなる
  ◆ 使い回したり標準の HTML タグみたいにあってほしいところだけで十分そう

WebComponents だったり React などのライブラリでコンポーネント化してウェブページを作ると 便利な部分も多いですが コンポーネントならではの困るところもあります
そんなコンポーネント化も辛いなーと思ったときの内容をまとめてみました

あくまでコンポーネントについてなので コンポーネントとは関係なく React や lit-html などの状態を変数で保持してそれを元にライブラリが DOM を構築する場合ならではの部分は含めません(form 関係とか別ライブラリの連携とか)

更新処理はどこでする

リストとリストアイテムのコンポーネントがあって リストアイテムのそれぞれには更新や削除ボタンがあります
DOM を含むローカルデータの更新や API を呼び出してサーバのデータの更新を行うときに その処理をどこに書くかについてです

コンポーネントの外

Redux などを使い データをコンポーネントの外で管理する方法を取っているなら そっちでやれば良いと思います
コンポーネントで状態を管理せず DOM を作るだけにしていれば この内容で困ることはないでしょう
ただ そういった仕組みを採用せずコンポーネントのみですべてを行う場合にどうするかです

親コンポーネントがする

親のリストコンポーネントがそれぞれのリストアイテムコンポーネントにデータを渡すので データを管理しているのは親のリストコンポーネントと言えます
親で管理する場合のやり方は

  • リストアイテムが削除イベントを dispatch して 親で listen
  • 親が子に削除関数を渡して 子はその関数を呼び出す

のどちらかになると思います

イベントを使うのは DOM らしい作りです
スタンダードな WebComponents だとこうなるかと思います

React だったり WebComponents でも lit-element を使う場合は コンポーネントが管理する DOM 内の子コンポーネントへプロパティを簡単に渡せます
イベントバブリングでルートまでイベントを伝える必要がなく親コンポーネントにだけ伝わればいいのなら 関数を渡す方法が多いと思います

どちらの場合であっても更新や削除の処理は親のリストコンポーネントで行います
そうなると親が全部の処理を知る必要があり 親コンポーネントが大きくなります
単純な追加・削除程度ならともかく 子のリストアイテムコンポーネントならではの処理があることもあります
子コンポーネントにボタンが追加されて 別の更新処理があるとそのたびに親コンポーネントに追加が必要です

このやり方では親コンポーネントは子コンポーネントに依存することになるので子コンポーネントを入れ替えにくくなります
その対処のためか 子コンポーネントに応じたデータを処理するクラスを用意して そのインスタンスを親コンポーネントが持つというのを見ることがあります
こうすれば親コンポーネント自体に処理を増やさなくて済みますし 子コンポーネントの種類に応じてデータを処理するクラスも変えれば 親コンポーネントは汎用的にできます

子コンポーネントがする

親からデータを受け取りますが 子コンポーネントがそのデータにどういうプロパティがあるなどを把握して 表示しているので 子コンポーネントがデータを管理しているとも見れます
それなら更新や削除もそのコンポーネントがやってしまうのもありです
そうすれば親コンポーネントはオブジェクトの配列として処理するだけで良くて 中にどんなプロパティがあるのかを知らなくてもいいです
親が子のすべてを管理するよりも子が自立してやってくれたほうが楽ですよね

React みたいな親が子にデータを渡すものなら 更新を親が知る必要があるので難しいですが WebComponents で DOM らしく作るのなら困りません
リストアイテムコンポーネントを作成するときに初期値として親がデータを渡しますが その後は子コンポーネントがデータを持ちます
input や textarea の value みたいなものです
親でデータが欲しくなれば リストコンポーネントがそれぞれのリストアイテムコンポーネントのプロパティにアクセスします
変更があったことを即座に知りたいなら 子がイベントを dispatch して親で listen します
削除する場合も WebComponents なら DOM のメソッドに remove があるので this.remove() で自分自身を消せます

デメリットもあって コンポーネント内部で完結するならまだしも API リクエストなど副作用を起こす処理をあちこちでされると複雑になってくると管理が辛くなってくるかもしれません
しかし 親コンポーネントでやったとしても リストコンポーネントがいっぱいあるかもしれませんし それを解決するなら親コンポーネントというよりもアプリケーション全体を 1 箇所で管理する方法にしたほうが良いと思います

子から親へ渡す場合

上のリストでは親が子に最初のデータを渡す前提でした
親から渡されるものはなくて 子コンポーネントがデータソースになることもあります
例えば検索ボックスをコンポーネントにする場合 検索する値はユーザが入力します
検索が実行されるとその親コンポーネントで検索ワードを元にフィルタをします
検索ボックスの入力のために検索ボックスコンポーネントで入力値を管理しますし リスト側でもフィルタのためにその値を保持します
親が子に渡すのではなく 子が親に渡すパターンになります

DOM 的な考えなら実体は子コンポーネントにあって親がそれを使うのはそれほど変には思いません
しかし React みたいなケースだと 子が親に渡すのは変な感じです
props は親から子の方向への一方向なので state として 2 重に管理することになりますし
こういうケースでも親で管理して 初期値は空文字として子の検索ボックスコンポーネントに渡せばいいのですが データが生まれるのは検索ボックスからのはずなので この辺りは DOM 系のほうが気持ちよく書ける気がします
あと 上の場合と同じように親で管理するなら親が子コンポーネントの数だけ更新処理を持つことになって複雑化します

プロパティ渡し

コンポーネント化してないときはどこでも好きなところを好きに変更できたのに コンポーネントになると離れたところを変更するのは一苦労です
React だと Context を使ってアプリケーション全体で共有したりできますし 以前書いた自作の WebComponents を使ったものでも全コンポーネントでアプリケーションで共有する値を参照できるようにしていました
こういう仕組みを用意すれば可能ではありますが すべてグローバルに比べると扱いづらくなります

レイアウト

コンポーネントを前提にした UI にしていれば プロパティ渡しもそこまで大変でもない気はしますが 実際にはそう行かないことが多いです
これまでのウェブページではコンポーネント前提の作りではなくグローバルにどこでも自由に書き換えられるのが当たり前でした
そのせいで コンポーネント的に考えればまとまるべきものがまとまらず分離していたりします
ちょうどそこが空いていたからと 更新ボタンが対応するフォームの最後ではなく グローバルなヘッダー部分に置かれていたり 全く関係ないところにあることもあります
HTML 的な構造を考慮せず 見た目やユーザの都合に合わせるなら仕方ないところではあります
そういうのをなくさず作りだけをコンポーネント化しようとするとコンポーネント化のメリットよりデメリットを強く感じることが多いです

レイアウトを誰かに決められるのならともかく 自分で好きに作るならあんまり影響ないと思ってましたが やっぱり先に見た目があってそれで動くように作っていくと コンポーネントに分けづらいことがあります

例えばこういう風に分けたいときがあります
白い部分と黒い部分がそれぞれ別のコンポーネントです

□□□□□□■■■
□□□□□□■■■
□□□□□□■■■
□□□□□□■■■
□□□■■■■■■
□□□■■■■■■
□□□■■■■■■

左右にそれぞれのコンポーネントで 中央部は上下半分でそれぞれに左側コンポーネント部分と右側コンポーネント部分になります
position とかで無理やりレイアウトしたり すればできなくはないでしょうけど コンポーネントに分けない場合に比べて複雑で扱いづらくなります

こういう風に分けてしまうのが簡単ですが ムダな空白がもったいないし イマイチな見た目になります

□□□□□□   ■■■
□□□□□□   ■■■
□□□□□□   ■■■
□□□□□□   ■■■
□□□   ■■■■■■
□□□   ■■■■■■
□□□   ■■■■■■

考えてみると ウェブ以外の最初からコンポーネント機能がある GUI ってほとんどはあまり見た目的に優れてはいないです
コンポーネントに分離してそれらを並べているだけと言ってもいいくらいなものをよく見ます

コンポーネント化すればそうなるのは仕方ないもので コンポーネントに囚われないからこそウェブが自由度のありすぎる見た目にできていたと考えると これまでのウェブの見た目にこだわるならコンポーネントは向いていないのではとも思えてきます
コンポーネント化して同じ見た目にできなくはなくても コンポーネント化によって作りやすくなる部分と同じかそれ以上に辛さもでてきます

機能や作りを変更する時

コンポーネントに分けていると 変更したいときに楽にできると思ってました
しかし 実際には変更する場所があちこちに分かれていて 変更のしやすさはそれほどメリットとして感じられませんでした
1 つのコンポーネント内に収まる変更であれば 楽なのかもしれませんが  1 つのコンポーネントに収まらない大きな変更になるとむしろ面倒に思います
コンポーネントを再編するようなこともあると苦労が増えてる気がします

静的型付けと同じ感じで 事前にどう作るかがしっかりと決まっていて 大きな変更はほとんどないような場合には向いてるように思います
それとは逆で作りながらどうするか決めていって 一度作った部分もどんどん変わっていくような作り方だとあまり向いてないと思います
そういう場合は変にコンポーネントに分けすぎない方がよさそうです
ほぼ変わらないことが確定しているような最小限の機能や使い回すパーツだけをコンポーネント化して それ以外は多少コンポーネントが大きくなってでも分割しないほうが変更するときに扱いやすいです

ただコンポーネントを大きくしすぎると React など仮想 DOM を使うものではパフォーマンスに影響するかもしれません
仮想 DOM ではどこが可変か事前にわからず 仮想 DOM 全体を比べて差分更新を行います
なので 小さくコンポーネントに分けて rerender が必要な範囲を最小限にして負荷を減らしています
それに比べて DOM の直接操作はもちろん 事前に変更箇所がわかっていて最小限の比較しかしない lit-html などではそこまで気にするものでもないです

使い回す?

コンポーネントは機能ごとに分けるだけではなくて使い回せるメリットもあります
しかし 使い回すかというと実際にはあまりそうでもないです
ヘッダーやページやフォームなどをコンポーネントに分けても基本その一箇所でしか使わないです
使い回すのは リストアイテムみたいな繰り返し系やタブやボタンみたいな部分くらいでしょうか

タブやボタンを使い回すと言っても 見た目が決まってしまうので 同じ見た目に揃えるそのアプリケーション内くらいでしか使えず 全く別のものを作るときには結局使いまわしていません
作ってるもののテーマに合わせた見た目にしたいので スタイルは毎回専用に作りたいです
ある程度なら使う側での CSS の上書きできなくはないですが コンポーネント側でいろいろな見た目を考慮してないと難しいです
例えばタブだとタブヘッダーが上に限らず下や左右にあることもありえます
デザインによっては 間に div を挟んでくれないと再現できないみたいなケースもありえます
それら全てに対応できるような万能コンポーネントにすると複雑になってメンテが辛くなります
そういうものを作ったとしても 実際にはそのアプリケーション内で使うスタイルはほぼ 1 つなので 結局そのとき専用に他の場合を考慮しないコンポーネントを作っています

それにタブやボタンみたいなもので 見た目をこだわらないなら UI ライブラリに任せればいいので自分で作るコンポーネントはそこ専用なものばかりです

全体をコンポーネント構成にする必要性

使い回さないのに コンポーネント化で面倒ごとが増えるなら それらをコンポーネント化する必要はないように思えてきます
そこまでしてコンポーネント化するメリットも特にないですし

そもそも WebComponents の場合は React みたいに細かく機能ごとに全部をコンポーネント化するという考えでもないと思います
ブラウザの標準の input や select や video みたいな特別な機能を持つ要素を作れるというだけです
パーツとして使いまわしたくなればコンポーネント化すればいいのであって 使い回すこともないのに機能的に分かれてるからという理由でその部分をコンポーネント化していく必要はないと思います

コンポーネントにまとめずグローバルに扱うとしても DOM 操作のみにしないといけないわけでもないです
lit-html で全体を render して 各コンポーネントは生の WebComponents でもいいですし WebComponents を lit-element で作ったり WebComponents 内部の描画に React や Vue を使うこともできます

作るものがこれまでと同じようなものだと コンポーネント化で全体的な作り方を変えてしまうよりも やりづらかった使いまわしを楽にできるもの くらいに使うほうがいいのかもしれません

WebComponents と view ライブラリ

最初に触れないと言いつつ 書いてみると WebComponents と React などのライブラリの違いが影響する部分が多かったです

考えてみると React などの view ライブラリってあくまで DOM を作り更新するだけのものなんですよね
lit-html はテンプレートライブラリと呼んでますし
Elm ではコンポーネントはなく DOM 定義を返すだけの view 関数です
React だとコンポーネントとは呼ばれてますが 個人的には使い始めた頃はコンポーネントと呼んでるのに違和感があったくらいです
クラスコンポーネントならメソッドを追加すれば WebComponents のようにやろうとすれば色々できなくはないでしょうけどインスタンスへのアクセスが難しいですし 基本的にはやらないと思います
関数コンポーネントだとできなくなるようなものですし 推奨されてないはずです

関数コンポーネントでは 一応フックで state を保持したり チューニングのためのメモとかはできるものの 基本は DOM 定義を返すだけの関数です
lit-html ではコンポーネント機能はなくテンプレートを返す関数とそれを render するだけの機能です
なので こういった view ライブラリでは機能を含めないのが正しい形なのかなと思います

対して WebComponents は DOM の要素ではありますが その内部での表示に限らず 機能を持ってるコンポーネントもあります
標準の HTML 要素でも video なら動画の再生やダウンロードができて form なら submit できてと単純に表示する以上の機能があります
今ではなくなったはずですが たしか秘密鍵を作る要素もあったと思います

それに合わせてか 以前 Polymer の WebComponents を見たときはたいていのことはコンポーネントの機能としてやってしまおうとしているように感じました
今では ES Modules で機能だけのモジュールをロードすれば良いと思いますが 以前は ES Modules がなかったので 各 script タグがカスタム要素の定義だけを行って DOM を介して機能を使えるほうが便利だったからかもしれません
URL の参照や書き換えもツリーにアタッチしたコンポーネントが行いますし 画面上の表示はなく機能だけを提供するコンポーネントもありました

ES Modules ありの標準の WebComponents では 表示とは関係しない部分は別モジュールとしてロードできるので画面に関係ないコンポーネントは減りそうです
それでもカスタム要素では 状態やロジックを持っていて DOM の機能と通常のクラスとハイブリッドのような扱いになると思います
この辺りが React のコンポーネントとは違うので まとめて「コンポーネント」として扱うのが良くなかったのかもしれないです