【React】コンポーネント実装時の留意点をまとめたい

公開:2021/01/29
更新:2021/01/29
2 min読了の目安(約2400字TECH技術記事

はじめに

Reactでコンポーネントを実装していく時に、どのような点に着目しどのように考えているのか、きちんと言語化しておきたいのでまとめていきます。

Container Component はまだまだ有用

React hooksの登場により「Container Componentは最早必要ない」との声も聞こえてくるようになりましたが、Viewと外部の世界を隔離するという点ではまだまだ有用です。
Viewと外部との接点をContainer Componentにまとめることで、Presentational Componentは「Propsを受け取ってJSXを返す」だけの純粋関数として構築できます。
それにより、StoryBookによるコンポーネントの管理も非常に簡単になります。

中規模以上のプロジェクトにはAtomic Designを導入

小規模なプロジェクトだと、コンポーネントの管理もそこまで気にする必要がありません。
しかしプロジェクトの規模が大きくなり、実装するコンポーネントの数が増えてくると、一定のルールの元でそれらを管理したくなってきます。その際はAtomic Designの導入を検討するべきでしょう。

Atomic Designを導入する場合、コンポーネントの分類について、デザイナーや他のエンジニアとの認識合わせは必須となります。 小規模なプロジェクトだとそこがコスト過多となりますが、中規模以上のプロジェクトとなってくると、そのコストを差し引いてもお釣りが返ってくることが多いです。(あくまで私の経験上ですが。)

Container ComponentはOrganismごとに作成する

Container componentを作成する粒度としては、Atomic DesignのOrganismが最適だと思っています。
Organismの「意味を持ち、独立して存在できる」という定義はStoreに接続してデータを引っ張ってくる際にも都合がよく、同じ文脈で用いるロジックを1箇所にまとめることもできます。
また、コンポーネントの階層も、Prop drillingを引き起こすほどの深さになりづらいです。
もし階層が深くなり過ぎてしまい、Propsの受け渡しが辛いのであれば、2つのOrganismに分割することを視野に入れていいかもしれません。

型定義は子コンポーネントに依存させる

Reactにおいて、データは上流のコンポーネントから末端のコンポーネントへと流れていきます。
それに対してコンポーネントの型定義は、末端から上流へと流れざるを得ません。

// 子コンポーネントA
export type ChildAProps = {
    foo:string
}
export const childA:FC<ChildAProps> = ({foo}) => {...}
// 子コンポーネントB
export type ChildBProps = {
    bar:string
}
export const childB:FC<ChildBProps> = ({bar}) => {...}
// 親コンポーネント
import {ChildAProps, ChildA} from './ChildA'
import {ChildBProps, ChildB} from './ChildB'

export type Parent = ChildAProps & ChildBProps
export const Parent:FC<ParentProps> = ({foo, bar}) => (
    <ChildA foo={foo}>
    <ChildB bar={bar}>
)

理由1 コンポーネントの独立性

Atomic Designにおいて、末端となるAtomsは他のどのコンポーネントにも依存せず、独立して存在することになります。その独立性によって、多様な場面でAtomsを使いまわすことが可能となるわけです。
必然的に、コンポーネントのインターフェイスたる型定義も、何者にも依存しないよう実装する必要があります。
親要素で定義された型をAtomsでimportしてしまうと、Atomsとしての独立性が損なわれますので、他の親要素でAtomsを利用する際に支障をきたします。

理由2 変更されにくいものに依存する

Clean Architectureでも、「変わりやすい要素は変わりにくい要素に依存せよ」と書かれています。
Atomic Designにおいて、「変わりにくい要素」とは、より末端に近いコンポーネントです。
末端に近いほど、他のコンポーネント内で利用されること多くなるため、その変更による影響は多岐に及びます。
Organisms内でAtomsやMoleculesの入れ替え、レイアウト変更を行うことはあっても、AtomsやMolecuresの実装自体をいじることは少ないです。

理由3 そもそもの依存の方向

子コンポーネントを組み合わせて親コンポーネントを作成する以上、親要素は子要素に依存します。
よって、型定義もその流れに従うのが自然ですね。

データ構造の型定義と振る舞いの型定義は分離する

特にフォームの実装時に顕著なのですが、データ構造と振る舞い(関数)の型定義は分離した方が管理しやすいことが多いです。特に経験の浅いメンバーがいる場合、useStateの複雑化や型ガードでの分岐時にしっちゃかめっちゃかになる事態を避けやすくなります。(この辺りはきちんとまとめきれていませんので、後ほど追記していきます。多分。)

終わりに

とりあえず思いついたことをまとめましたが、今後も考えがまとまり次第追加&修正していきます。