Modalの開閉ステートを一元管理したい

## はじめに

一つのPageで複数種類のModal開閉が必要になり、それぞれのStateを定義してコードを記述する場面は意外と多いと思います。
カスタムフックに切り出してリファクタリングができることを学んだので使いまわせるカスタムフックにできないか挑戦してみた記録を残します。

## 結論
ジェネリクスも使えば使いまわせるカスタムフックが作れる。
ただし、一つのファイルからしか使わないのであればやりすぎかも。

## 最終形
ジェネリクスを使って汎用的にしたカスタムフックは以下の通り。

```typescript
import { useState } from 'react'

export const useModalManager = <T extends string>() => {
  const [modals, setModals] = useState<Record<T, boolean>>({} as Record<T, boolean>)

  const openModal = (key: T) => {
    setModals((prev) => ({ ...prev, [key]: true }))
  }

  const closeModal = (key: T) => {
    setModals((prev) => ({ ...prev, [key]: false }))
  }

  const isModalOpen = (key: T) => modals[key] || false

  return { openModal, closeModal, isModalOpen }
}
```

呼び出す時は、
```typescript
const {
  openModal,
  closeModal,
  isModalOpen
  } = useModalManager<'hoge' | 'fuga' | 'piyo'>()
```

別ファイルで新たに呼び出す時、
```typescript
const {
  openModal,
  closeModal,
  isModalOpen
  } = useModalManager<'HOGE2' | 'FUGA2'>()
```

とすれば、他のファイルに影響を与えずに使える。
コンポーネント内での使い方については後述します。

## 変遷(初期)
モーダル開閉のbooleanをオブジェクトにして一括で管理したいというのが基本コンセプト。
最初は以下のようなカスタムフックを作成しました。

```typescript
import { useState } from 'react'

type ModalKeys = string

export const useModalManager = () => {
  const [modals, setModals] = useState<{ [key: string]: boolean }>({})

  const openModal = (key: ModalKeys) => {
    setModals((prev) => ({ ...prev, [key]: true }))
  }

  const closeModal = (key: ModalKeys) => {
    setModals((prev) => ({ ...prev, [key]: false }))
  }

  const isModalOpen = (key: ModalKeys) => modals[key] || false

  return { openModal, closeModal, isModalOpen }
}
```

この時点でほぼ形はできていたのですが、
View側で呼び出す時に、
```typescript
{isModalOpen('hoge') && (
  // modalの中身
)}
```
のようにkeyを引数として与える際、
コードエディタがサジェストをしてくれない課題に直面しました。
サジェストがないと、タイポでしょうもないエラーとか起こしかねないのでそれだけは避けたい…
ということで次の形に変更しました。

## サジェストを出したくて(中期)
ModalKeyの型がstring型になっているのでサジェストが出ないので、親のコンポーネントで呼び出す文字列に限定しちゃおうというのが以下の形。

```tsx
type ModalKeys = 'hoge' | 'fuga' | 'piyo'

export const useModalManager = () => {
  const [modals, setModals] = useState<{ [key: string]: boolean }>({})

  const openModal = (key: ModalKeys) => {
    setModals((prev) => ({ ...prev, [key]: true }))
  }

  const closeModal = (key: ModalKeys) => {
    setModals((prev) => ({ ...prev, [key]: false }))
  }

  const isModalOpen = (key: ModalKeys) => modals[key] || false

  return { openModal, closeModal, isModalOpen }
}
```

こうすることで、
`openModals('hoge')` などの引数を記述する際に３種類のサジェストが出るように。
こうなればコーディングの時にタイポを気にせずTabキーで選択すればいいのでストレスが激減しました。

結構使い勝手のいいカスタムフックだなーとなったらさらに欲が出て、
せっかくカスタムフックとして切り出しているのだから他のコンポーネントでも使いたい！
となるわけで…。

でも、今の記述方法では、使える文字列が `'hoge' | 'fuga' | 'piyo'` に限定されているのでもう一段階汎用性を引き上げる必要が出てきました。

## そして最終形へ
カスタムフックに引数として文字列のリストを与える方式も考えたのですが、現在のカスタムフックから変更が多くなりそうだったので結論で記述したジェネリクスを使用する方式で行くことにしました。

(↓ 再掲)
```typescript
import { useState } from 'react'

export const useModalManager = <T extends string>() => {
  const [modals, setModals] = useState<Record<T, boolean>>({} as Record<T, boolean>)

  const openModal = (key: T) => {
    setModals((prev) => ({ ...prev, [key]: true }))
  }

  const closeModal = (key: T) => {
    setModals((prev) => ({ ...prev, [key]: false }))
  }

  const isModalOpen = (key: T) => modals[key] || false

  return { openModal, closeModal, isModalOpen }
}
```

元々以下のようなコードがあったとして、
(注：サンプルなので実際に動かしてはないです。雰囲気だけ掴んでもらえたら幸いです。)
```tsx
export const SampleComponent = () => {
  const [isShownHogeModal, setIsShownHogeModal] = useState<boolean>(false)
  const [isShownFugaModal, setIsShownFugaModal] = useState<boolean>(false)
  const [isShownPiyoModal, setIsShownPiyoModal] = useState<boolean>(false)

  return (
    <>
      <button onClick={() => setIsShownHogeModal(true)}>a</button>
      <button onClick={() => setIsShownFugaModal(true)}>b</button>
      <button onClick={() => setIsShownPiyoModal(true)}>c</button>

      {isShownHogeModal && (
        <HogeModal closeThisModal={() => setIsShownHogeModal(false)} />
      )}
      {isShownFugaModal && (
        <FugaModal closeThisModal={() => setIsShownFugaModal(false)} />
      )}
      {isShownPiyoModal && (
        <PiyoModal closeThisModal={() => setIsShownPiyoModal(false)} />
      )}
    </>
  )
}
```

これに対して今回作ったカスタムフックを適用すると、
```tsx
export const SampleComponent = () => {
  const { openModal, closeModal, isModalOpen } = useModalManager<'hoge' | 'fuga' | 'piyo'>()
  return (
    <>
      <button onClick={() => openModal('hoge')}>a</button>
      <button onClick={() => openModal('fuga')}>b</button>
      <button onClick={() => openModal('piyo')}>c</button>

      {isModalOpen('hoge') && <HogeModal closeThisModal={() => closeModal('hoge')} />}
      {isModalOpen('fuga') && <FugaModal closeThisModal={() => closeModal('fuga')} />}
      {isModalOpen('piyo') && <PiyoModal closeThisModal={() => closeModal('piyo')} />}
    </>
  )
}
```

別ファイルに対しても呼び出す型を変更すれば、keyの値を他に影響させることなく自由に変更することができるのでいい感じになったような気がします。


## 終わりに
ジェネリクスが苦手でまだまだ自由に使いこなせないのですが、一つずつ試しに書いてみて理解を深められたらいいなと思っています。