thumbnail

【React】コンポーネントに汎用性をもたせる際のポイント

2022/09/28

ReactやVueの開発においては、コンポーネントの汎用性は重要な観点になります。

今回はコンポーネントに汎用性を持たせるポイントを紹介しますが、前提として汎用性がないコンポーネントが悪というわけではなく、そもそもあまり使い回さないコンポーネントであれば汎用性とかは気にする必要はないと個人的に思っています(汎用的じゃないコンポーネントのほうが実装しやすいですし)。

ただし、最初は汎用性が必要なくても実装していると後から汎用性をもたせる必要が出てくる場面が結構あります。

その際に、誤った汎用化をしていると後々、保守・改修等で困ることになるので、そうならないために汎用化のコツを具体例を交えて紹介します。

例:ListコンポーネントとListItemコンポーネント

例として以下のような一覧のコンポーネントを実装するパターンを考えます。

react-component-list-example

はじめに汎用性のないListItemコンポーネントを紹介し、その後にListItemコンポーネントの汎用化を行っていきます。

汎用性のないListItem

今回定義するListItemコンポーネント、Listコンポーネントは以下のとおりです(あえて汎用性がない書き方をしています)。

ListItem.tsx
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
List.tsx
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として渡すのはオススメしません。

ListItemtype Itemに強く依存しているため、汎用性がありません。 ListItemListしか使わない場合は問題がありませんが、このListItemを他のコンポーネントでも使いたいとなると少し大変です。

例えば以下のようなList2コンポーネントでListItemを使用するとなるとどうでしょうか?

List2.tsx
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を使えるようにするため、以下のように変更する必要がでてきます。

ListItem.tsx
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にsetStatemutateを直接渡さない
  • Propsに渡すオブジェクトを適当な属性に分解して渡す

↑の点を踏まえて変更するとListItemは以下のようになります。

ListItem.tsx
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を直接渡すのではなくlabelvalueとして細かく渡すようにしています。 また、setStateを直接渡すのではなくonClickDelete関数を渡すように変更しています。

ListList2コンポーネントからListItemコンポーネントを使用するときは以下のようになります。

List.tsx
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
List2.tsx
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

これによりList3List4などがでてきてもListItem以下のコンポーネントを修正する必要がなくなりました。

このような汎用化をどのタイミングで行うかは人それぞれだと思いますが、個人的にはItemだけではなく、Item2でも使うとなった時点で上記のような汎用化を行います。

おわり

とりあえずPropsに渡す型を増やすという方法で汎用化を行うのはやめたほうがいいかもしれません

author picture

Mitsuru Takahashi

京都市内にてフリーランスエンジニアとして活動しています。

detail

Profile

author picture

Mitsuru Takahashi

京都市内にてフリーランスエンジニアとして活動しています。

detail

© 2022 mitsuru takahashi