ReactComponentのベストプラクティス
結論は、「Presential
も、Container
も、StyledComponent
も全部まとめて書きたい!」です。
Reactでコンポーネントを書くときに、Presential
とContainer
の役割を分離するというのが基本みたい。 ファイルそのものを分割してそれぞれに役割を持たせるというのがスタンダード。
ただ、CSS-inJSでスタイルも同じファイルに定義できるようになった今、ファイルを分割しすぎるのもちょっと扱いにくい。 ロジックとビューに改修を入れたい場合、複数のファイルを見に行く必要があり、面倒に感じてしまう。。
そこで参考にさせていただいたのがこちらの記事
DOM,Style,Containerで分離させる
// (1) import層
import React from 'react'
import styled from 'styled-components'
// (2) Types層
type ContainerProps = {...}
type Props = {...} & ContainerProps
// (3) DOM層
const Component: React.FC<Props> = props => (...)
// (4) Style層
const StyledComponent = styled(Component)`...`
// (5) Container層
const Container: React.FC<ContainerProps> = props => {
return <StyledComponent {...props} />
}
1つのファイルで技術を分離させて、それぞれの役割をさせているといった感じ。
DOM層はpropsを受け取ってDOMに反映するだけ。ロジックを持たず、ステートレス(状態を持たない)レイヤー。 まだやったことはないが、constで定義されているので、コンポーネントだけを抜き出してテストすることが簡単にできる。
Style層はDOMComponentを読み込んでStyledComponentを適用させる。個人的にはクラス名をつけてBEMの記述をするのがベストだと思っている。
理由は、> tag
のようにComponentのchildren以外へのスタイル適用を防いだりする必要がないし、CSSModuleへの移行も簡単にできるから。
ContainerComponentは、明確にビジネスロジックが入る。useEffectだったり、stateの操作を行うステートフルな部分。
1ファイルにまとまっていてかつそれぞれの役割が明確に分離されているのが個人的には理解しやすかった。
上記参考にしたComponent例
import React from "react";
import styled from "styled-components";
/*
Component名を定数で定義
Component名がBEMのBlock要素としてクラス名を定義するため。
外部ComponentにComponentスタイルの影響を与えないため
*/
const COMPONENT_NAME = "User";
/*
ContainerのPropsを定義
親Componentから渡されたpropsはContainerで読み込まれるため、こちらで定義する。
GatsbyのGraphQLもこちらで。
*/
type ContainerProps = {
user: {
name: string;
age: number;
};
};
/*
ContainerからDOMComponentへ渡すprops
*/
type Props = {
hello: string;
handleClick: (name: string) => void;
} & ContainerProps;
/*
DOMComponent
*/
const Component: React.VFC<Props> = (props) => (
<div className={`${COMPONENT_NAME}__container`}>
<p className={`${COMPONENT_NAME}__name`}>{props.user.name}</p>
<p className={`${COMPONENT_NAME}__age`}>{props.user.age}</p>
<button onClick={() => props.handleClick(props.user.name)}>Click!</button>
<p className={`${COMPONENT_NAME}__sayHello`}>{props.hello}</p>
</div>
);
/*
StyledComponent
Component名をBlock要素として、BEMで記述する
*/
const StyledComponent = styled(Component)`
.User__name {
padding: 8px;
font-size: 16px;
font-weight: bold;
}
`;
/*
ContainerComponent
受け取ったpropsをDOMComponentへ渡す
あるいは、ロジックを加えたものを渡す
state、useEffect等のすべてのロジックはここに記述
*/
const Container: React.FC<ContainerProps> = (props) => {
const [hello, setHello] = React.useState("");
const handleClick = React.useCallback(
(name: string) => {
setHello(`Hello! ${name}!!`);
},
[setHello]
);
return <StyledComponent {...props} handleClick={handleClick} hello={hello} />;
};
/*
ContainerComponentをdefault exportする。
Containerが無いComponentの場合は、
export default StyledComponent; になる
*/
export default Container;
ディレクトリ構造とファイル名
仕事ではAtomicDesignを使用している。
コードが長くなったり、見にくいなーと感じたら、Componentに切り分けたりすることもあるのですが、コードレビューを受けると、「使い回さない部分はComponentに分ける意味ないですよね」みたいな意見があった。
AtomicDesign に倣うこともありますが、いきなりそれに沿った構成にすることはありません。「特定の箇所を変更したい場合、誰でも想像したとおりの場所にある」という感覚を特に大事にしています。たとえば、要素リストを含む Component は次の様な構成にします。この様な単純な名称であれば、どこを修正すれば良いのか一目瞭然です。 経年劣化に耐える ReactComponent の書き方
Users
├── index.tsx
├── title.tsx
└── list
├── index.tsx
├── title.tsx
└── item
├── index.tsx
├── title.tsx
├── avatar.tsx
└── icon.svg
確かに、この構成だとめっちゃわかりやすい。 それぞれのindex.tsx
がエントリーポイントになっていて、同階層のComponentを読み込んでいるイメージだと思う。
また、必要なファイルがディレクトリにまとまっているので、 AtomicDesignのように/molecules
と/organisms
と/atoms
と行ったり来たりしなくていいのがいい!
AtomicDesign に倣う場面は、これが複数のComponentで利用される様になったタイミングです。はじめからそれに倣うと、オーバーエンジニアリングになるため、複数のComponentで利用されるタイミングで、共有Component ディレクトリへと移します(リファクタ)。限定的コンテキストに閉じられた Component であるならば、はじめから AtomicDesign のルールに則ることが却って足枷になると感じています。 経年劣化に耐える ReactComponent の書き方
いきなりAtomicDesignにしないのがポイントかも。プロジェクトの進捗によって、Componentが必要とされるタイミングでAtomicDesignへ落とし込む(リファクタリング)
AtomicDesignを採用していて、共通化できてないComponentって、どうやって管理しているんだろうか?templates
とかでまとめてるのか。 上記の例みたいに、ディレクトリを切った構成にするのもありかなと思った。
参考リンク
ReactComponentのベストプラクティスを考えてみた