【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 ListItem
const 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 List
ListItem
の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 List2
List2
コンポーネントでも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 ListItem
Propsの変更と、型によって属性名が変わるのでそこの対応も行いましたが、これはあまり良くない汎用化です。
今後、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 ListItem
Propsに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 List
type 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に渡す型を増やすという方法で汎用化を行うのはやめたほうがいいかもしれません