component
書いてますか?
また Advent Calendarの季節がやってきましたね。Webフロントエンドエンジニアの原田です。
今回の記事では、Webアプリケーションを作る際の検討事項で、「 React の component
を扱うのって難しいなあ」という素朴な話をします。
「こうすれば最高よ」という記事は、熟達のプロフェッショナルが書いてくれてたりしますが、迷ってる記事は、少ないような
私は「こうすれば最高よ」という記事を見て、「確かに最高そう!」と安易にやった結果、大怪我したことがあります。
なので今回は素直に、「難しいなあ、、」という記事を書いてみようと思います。
一応役に立ったら嬉しいなということで、「難しいなあ、、」と思った末に採択した結果も書きますが、あくまで経緯をすっ飛ばした上での「今の気分」です。
「悩むもんなんだな〜」とエッセイのようにお読みいただけるといいかなと思います。
component
とは
この記事では、UIを構成する一要素を返却する関数
くらいで捉えることにします
コンポーネントにより UI を独立した再利用できる部品に分割し、部品それぞれを分離して考えることができるようになります
概念的には、コンポーネントは JavaScript の関数と似ています。(“props” と呼ばれる)任意の入力を受け取り、画面上に表示すべきものを記述する React 要素を返します。
注記
Webアプリケーションを作って運用するには、component
の他にも「難しいなあ、、」 という要素が無限にありますが、「component
だけでも難しいぞ、、」という雰囲気が伝わればと思います。
では行ってみましょう。
テストが難しい
Jest のほか、React Testing Library など、ツールチェインは揃っているが、はてどう書いたら正解なんでしょうね。
ロジックはhooks
に切り出して、ユニットテストも書いた方がナウい気もするけれど、(インテグレーションテストに属する)ビジュアルリグレッションテストを導入するなら、担保する部分被っちゃうのでは、、とか悩んだりしました。
Storybook を利用しつつ、成果物を利用してテストを書く流派もあるらしいので、それも魅力的に思えてきたりして。
今の気分
reg-suit のような、ビジュアルリグレッションテストに注力するのが今の気分です。
Storybook では、そんなに苦労することなく story (見た目のパターン)が書けます。
Storybook に関しては、MSW 登場後は、APIとの疎通までモックできるようになりました。
モックを書くコストは必要になりますが、Storybook 上で作成を進められる範囲が広がるので、開発体験もよく、ついでに安心感も得られて嬉しい、と思い推しています。
story への反映を 1PR の目標に組み込むなど、地道なことをやりつつ網羅率アップを図っています。
ちなみに、 Jest + React Testing Library を使ったテストも書くことはありますが、「 Storybook で担保し切れない & 前提条件の用意が面倒なモノ」 に対してくらいですかね、、、。
「hooks
なり関数なりへのテストを、積極的には入れない方針の方が、カジュアルに切り出し関数の分解点を変更できて、それはそれでアリかも」くらいのノリで生きています。
注記
Storybook では、そんなに苦労することなく story (見た目のパターン)が書けます。
ゆうて、可能な限り表示パターンを網羅する必要があったり、うまくリグレッションテストが働かない( Flaky になってしまう)ことへの対処も必要になってくるので、コストかかるといえばかかります。
ただ、ページに埋め込んだ状態で人力網羅を繰り返すよりは安いのでは、と感じています。
よもやま話: Storybook を安易に使うと荒地ができる
reg-suit を使うまでは、「 Storybook って何のために使ってるんだろう、、」と思うことがありました。
UI カタログあったら便利そう = 結局ページに表示して動作確認しちゃう = story 書くの忘れる = 網羅率が悪くなる = 荒地が残る(でもたまに思い出したように更新してみたりする)、、というパターンですね。
こうなったらあっても無くても一緒、いやむしろ「更新作業があるのでマイナスまであるのか、、、?」と頭に浮かぶこともありました。
Storybook は強いツールですが、「何のために使うんだっけ?(テストとして? UI カタログとして? もしくはその両方として?)」を意識して運用する覚悟決めないと、足かせにもなり得るんですねえ、怖いですねえ、、、
よもやま話: Container / Presentation パターンは古い?
Danさんも「hooks
ができたから無理してこれ採用しなくていいよ」と言ってます。
私も「古いのか、、」と思い悩んでいましたが、テスト観点で嬉しい特典があるので、「アリですね」が今の気分です。
例えば Storybook について、「 MSW が使える」、「 play function が使える」と言いつつ、一番 story が書きやすいのは、 Container / Presentation の Presentation なことに変わりはないと思っています。
ロジック持たない = 大抵のことが引数で表現できる = さっと安定した story が出来る、ということです。
使える時間は限られているので、パターンを作るのに労力がかかると、「まあ今回は作らないでも、、」と楽な方に行きがちに感じています。
何を大事にしたいかで、component
の設計手法を決めていきたいな、というのが今の気分です。
再利用が難しい
再利用と言いますが、例えばデザイナーさんが違うボタン作ったら、既存のボタンは再利用できません。
そうして component
は増えていきます。
こうなってくると、探すよりも作った方が早くなる場面が多くなります。
逆に、それを嫌って意地でも同じ component
を使い続けると、「 story 何個あるんだよ」っていうお化けボタンが出来上がったりもします。
あと、「見た目は同じなんだけど、ちょっとだけ動作が違う、、」ユースケースを持つcomponent
は、それぞれのユースケースを考慮して書かねばならず、別々で書くよりコストがかかることもあります。
そうして3つくらい再利用と魔改造された後、4つ目は新規で書かれたりして、5つ目を作るときに、「どう作るのが正解なんだ?」と悩んだりするわけですね。
あっちを立てればこっちが立たず、、
あとは単純に、「途中から入ったので、そんな component
があるなんて知りませんでした」とか
今の気分
あんまり再利用のことはシビアに考えないほうが幸せなんじゃないか、という気分です。
再利用が目的になっちゃうと、「これは同じ部品として使いたいぞ」と無理した結果、逆に再利用しにくい物体が出来上がる、皮肉なやつになります(なったことがあります)。
無理せず伸び伸びと「これは一体何者なんだろう」と考えたり調査したり相談して分割した結果、「自然と使っちゃいますねこのcomponent
」となれば儲け物、くらいで良いのではないでしょうか。
再利用できないことも正しいことはあるんですよきっと、、
ポエムじみてきました。いいですね。次いきましょう。
よもやま話: デザインシステム or デザインガイドラインは難しい
何かのきっかけで、デザインシステム or デザインガイドラインの導入が検討されることがあります。
どちらにせよデザインに一定のパターンができて、いろいろ嬉しいわけではありますが、これも運用はなかなか難しい、、。
下記、身に覚えないでしょうか、、(私はあります)
- エンジニアがデザインシステムに無いオリジナル単位を埋め込んだ(ただ迂闊なだけだが、ちりつもで破綻する)
- デザインガイドラインで賄えない要件が出てきた
- デザイナーとエンジニアの考える再利用可能単位(
component
の分割点)が違う - デザインシステム or デザインガイドラインを無視した要件が来る(必要に思われていない or 理解する余裕がない or こちらから攻めに行ってない)
また、こちらも Storybook と同じく、あればいいかというとそうでもなく、「なんとなくあるらしい」程度だと悩みの種になったりします。
「デザインガイドラインと違うけど、意図的なのかな?聞いたほうがいいのかな?」みたいな
幸か不幸か、新雪のような デザインシステム or デザインガイドライン に足を踏み入れたことはないですが、もしきっかけがあれば、大切に育てていきたいですね。
ちなみに デザインシステム or デザインガイドライン に関しては、弊社に素晴らしい記事があるので、興味ある方はご一読ください。
https://blog.nijibox.jp/article/design_system/
https://blog.nijibox.jp/article/business_creative_11/
配置が難しい
どのディレクトリにどんなcomponent
をどう置くのか、という話です。
Atomic Designに沿って配置してみるとか、いややっぱ違うぞとか、Atomic Designに立脚しつつも、より明確な線引きを求めようとか、機能ごとに切っとけばええんちゃうとか、はてどうしたものやら、、
この辺流派が色々ある上に、開発効率にも繋がるので、頭を悩ませてくるポイントなんですよね。
置くだけなのになあ。。。
間違えると下記のようなことが起こるんですよ。
- 同じタイミングで変更するものが近くにない
- 変更作業が面倒くさい
- 明確なルールがない、お気持ちディレクトリ構造
- 新たにジョインした人が、さっと
component
の場所を決定できない
- 新たにジョインした人が、さっと
- 直感的に
component
を探せない- 新たにジョインした人が、利用できそうな
component
の有無を判断することができない
- 新たにジョインした人が、利用できそうな
今の気分
難しいなあと思っています
ただ、アンチパターンっぽいことはわかってきたので、それを避けたい気持ちはあります。
汎用component
と 固有component
は一緒にしないほうがいい
両者でレビューの態度が違ったりします。
汎用は使い勝手をつぶさに見たいけど、固有はさくっとLGTMしちゃうとか。
影響範囲の想像がつきやすいディレクトリ構造で分割されていると、身構えやすいなあと思っています。
それ(命名によって影響範囲が大体分かる)ができてテストがええ感じになっていると、ありがたさが出てきます
hooks
みたいなディレクトリは集約させないほうがいい
前段の話とも通じますが、「複雑なロジックを切り出したい」くらいの欲求で作るものと、共通で使わなければいけないものは分けたいなあという気持ちです
├── components
│ ├── SomeComponent
│ │ ├── index.stories.tsx
│ │ ├── index.tsx
│ │ ├── SomeComponent.stories.tsx
│ │ ├── SomeComponent.tsx
│ │ ├── SomeComponent.css
│ │ └── useSomeHooks // 処理を抜き出しただけ
│ │ └── index.ts
│ │
│ ├── hooks
│ │ └── useApi // ゆうて共通にしたい、抽象化とか腐敗防止層とかに言い換えられるようなものもある
│ │ └── index.ts
│ │
型が難しい
TypeScript が難しいのではなく(いや難しいんですが)、どこにどんな型をどう置くのがええの、、という話です。
一旦 component
の props の話だけをします
こんな component
があるとします
const SomeComponent: FC<Props> = ({ hoge }) => {
return (<div>
<AnotherComponent hoge={hoge} />
</div>)
}
ここの Props
はどこに書くといいんでしょうか?
①同じファイルで定義
type Props = {
hoge: {
fuga: string
}
}
const SomeComponent: FC<Props> = ({ hoge }) => {
return (<div>
<AnotherComponent hoge={hoge} />
</div>)
}
②別ファイルで定義
export type Props = {
hoge: {
fuga: string
}
}
import { Props } from './types'
const SomeComponent: FC<Props> = ({ hoge }) => {
return (<div>
<AnotherComponent hoge={hoge} />
</div>)
}
③利用component
から、ComponentProps
で引っ張る
type Props = ComponentProps<typeof AnotherComponent>
const SomeComponent: FC<Props> = ({ hoge }) => {
return (<div>
<AnotherComponent hoge={hoge} />
</div>)
}
④APIからデータを受け取って表示するやつなので、その型からとる
type Props = {
hoge: FugaAPIResponse
}
const SomeComponent: FC<Props> = ({ hoge }) => {
return (<div>
<AnotherComponent hoge={hoge} />
</div>)
}
今の気分
難しいなあと思っています
ただこれも、ままならない場合は分かってきました。
別ファイルで定義
別にいいっちゃいいんですが、迂闊な場合にカオスになったりします
export type Props = {
hoge: {
fuga: string
}
}
import { Props } from './types'
const SomeComponent: FC<Props> = ({ hoge }) => {
return (<div>
<AnotherComponent hoge={hoge} />
</div>)
}
import { Props } from '../SomeComponent/types' // いや共有したくなかったんだけど? という気持ち
const AwesomeComponent: FC<Props> = ({ hoge }) => {
return (<div>
<AnotherComponent hoge={hoge} />
</div>)
}
利用component
から、ComponentProps
で引っ張る
利用する 子component
が変わった瞬間に、変えなきゃいけなくなります。
多分、「利用している component
のprops定義が変わったら、自動で変わって欲しいぞ」と思ってこうするわけですが、これがよろしくないんですね。
一回そんな目に合うと、「独立させて書いておけばよかったなあ、、」と思います。
「この component
は文字を表示するものなのだ」のように、子component
に依存しない役割であれば、同じファイルに書いちゃっていいのかな、と思います。
「この component
は、あるcomponent
をラップする何かだ」という場合は、有用なこともありそうですね
APIからデータを受け取って表示するやつなので、その型からとる
利用する component
が他のユースケースで使われた瞬間に、意味をなさなくなります。
多分、「props書くのだるいなあ」とかでこうなるわけですが、これがよろしくないんですね。
これが有用な時は、、どうなんだろ、あるんですかねえ、、
同じファイルで定義
結果これが今の気分です。
一番いいぞというよりは、まあ普通で、そんなに困ったこともないなあ、、というところですね
まとめ
「 React の component
を扱うのって難しいなあ」って気持ち、伝わりましたかねえ、、
仮に「これもう楽勝ですよ、これ使ってれば勝てます」があれば、こっそり教えてくださいね、、
それではよい年末を