
【React】コンポーネントに汎用性をもたせる際のポイント
2022/09/28
ReactやVueの開発においては、コンポーネントの汎用性は重要な観点になります。
今回はコンポーネントに汎用性を持たせるポイントを紹介しますが、前提として汎用性がないコンポーネントが悪というわけではなく、そもそもあまり使い回さないコンポーネントであれば汎用性とかは気にする必要はないと個人的に思っています(汎用的じゃないコンポーネントのほうが実装しやすいですし)。
ただし、最初は汎用性が必要なくても実装していると後から汎用性をもたせる必要が出てくる場面が結構あります。
その際に、誤った汎用化をしていると後々、保守・改修等で困ることになるので、そうならないために汎用化のコツを具体例を交えて紹介します。
例:ListコンポーネントとListItemコンポーネント
例として以下のような一覧のコンポーネントを実装するパターンを考えます。

はじめに汎用性のないListItemコンポーネントを紹介し、その後にListItemコンポーネントの汎用化を行っていきます。
汎用性のないListItem
今回定義するListItemコンポーネント、Listコンポーネントは以下のとおりです(あえて汎用性がない書き方をしています)。
type Item = {
id: string
name: string
value: number
}
type ListItemProps = {
item: Item
setItems: React.Dispatch<React.SetStateAction<Item[]>>
}
const ListItem: React.FC<ListItemProps> = (props) => {
const { item, setItems } = props
const onClickDelete = () => {
setItems((prev) => prev.filter((i) => i.id !== item.id))
}
return (
<div>
<p>{item.name}</p>
<p>{item.value}</p>
<button onClick={onClickDelete}>Delete</button>
</div>
)
}
export default ListItemconst List: React.FC = () => {
const [items, setItems] = useState<Item[]>([])
return (
<ul>
{items.map((item) => (
<li key={item.id}>
<ListItem item={item} setItems={setItems} />
</li>
))}
</ul>
)
}
export default ListListItemのPropsにはitemとitemのdeleteを押した時にitemsから省くようにsetItemsを渡しています。
※ ここではあえてsetItemsを直接渡していますが、親コンポーネントからしたらsetItemsをどのタイミングで何に使うのかわからないので、基本的にsetState系を直接Propsとして渡すのはオススメしません。
ListItemはtype Itemに強く依存しているため、汎用性がありません。
ListItemをListしか使わない場合は問題がありませんが、このListItemを他のコンポーネントでも使いたいとなると少し大変です。
例えば以下のようなList2コンポーネントでListItemを使用するとなるとどうでしょうか?
type Item2 = {
id: string
name: string
price: number
}
const List2: React.FC = () => {
const [items, setItems] = useState<Item2[]>([])
return (
<ul>
{items.map((item) => (
<li key={item.id}>
<ListItem item={item} setItems={setItems} />
</li>
))}
</ul>
)
}
export default List2List2コンポーネントでもListItemを使えるようにするため、以下のように変更する必要がでてきます。
type ListItemProps = {
item: Item | Item2
setItems:
| React.Dispatch<React.SetStateAction<Item[]>>
| React.Dispatch<React.SetStateAction<Item2[]>>
}
const ListItem: React.FC<ListItemProps> = (props) => {
const { item, setItems } = props
const isTypeItem = (item: any): item is Item => {
return item.value !== undefined
}
const onClickDelete = () => {
setItems((prev) => prev.filter((i) => i.id !== item.id))
}
return (
<div>
<p>{item.name}</p>
<p>{isTypeItem(item) ? item.value : item.price}</p>
<button onClick={onClickDelete}>Delete</button>
</div>
)
}
export default ListItemPropsの変更と、型によって属性名が変わるのでそこの対応も行いましたが、これはあまり良くない汎用化です。
今後、Item3などの新たな型に対応しようとすると型判定の処理がより複雑化し、
かつ、ListItem配下でItemに依存するようなコンポーネントが複数連なっている場合、その全てに対して同じような面倒くさい変更をしていく必要があるからです。
ListItemの汎用化
次にListItemをより汎用的なコンポーネントに変更します。
コンポーネントに汎用性をもたせる際のポイントは以下の2つです。
- Propsに
setStateやmutateを直接渡さない - Propsに渡すオブジェクトを適当な属性に分解して渡す
↑の点を踏まえて変更するとListItemは以下のようになります。
type ListItemProps = {
label: string
value: number
onClickDelete: () => void
}
const ListItem: React.FC<ListItemProps> = (props) => {
const { label, value, onClickDelete } = props
return (
<div>
<p>{label}</p>
<p>{value}</p>
<button onClick={onClickDelete}>Delete</button>
</div>
)
}
export default ListItemPropsにitemを直接渡すのではなくlabel、valueとして細かく渡すようにしています。
また、setStateを直接渡すのではなくonClickDelete関数を渡すように変更しています。
List、List2コンポーネントからListItemコンポーネントを使用するときは以下のようになります。
type Item = {
id: string
name: string
value: number
}
const List: React.FC = () => {
const [items, setItems] = useState<Item[]>([])
const onClickDelete = (id: string) => () => {
setItems((prev) => prev.filter((i) => i.id !== id))
}
return (
<ul>
{items.map((item) => (
<li key={item.id}>
<ListItem
label={item.name}
value={item.value}
onClickDelete={onClickDelete(item.id)}
/>
</li>
))}
</ul>
)
}
export default Listtype Item2 = {
id: string
name: string
price: number
}
const List2: React.FC = () => {
const [items, setItems] = useState<Item2[]>([])
const onClickDelete = (id: string) => () => {
setItems((prev) => prev.filter((i) => i.id !== id))
}
return (
<ul>
{items.map((item) => (
<li key={item.id}>
<ListItem
label={item.name}
value={item.price}
onClickDelete={onClickDelete(item.id)}
/>
</li>
))}
</ul>
)
}
export default List2これによりList3、List4などがでてきてもListItem以下のコンポーネントを修正する必要がなくなりました。
このような汎用化をどのタイミングで行うかは人それぞれだと思いますが、個人的にはItemだけではなく、Item2でも使うとなった時点で上記のような汎用化を行います。
おわり
とりあえずPropsに渡す型を増やすという方法で汎用化を行うのはやめたほうがいいかもしれません
