0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

結論

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

最終形

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

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 }
}

呼び出す時は、

const {
  openModal,
  closeModal,
  isModalOpen
  } = useModalManager<'hoge' | 'fuga' | 'piyo'>()

別ファイルで新たに呼び出す時、

const {
  openModal,
  closeModal,
  isModalOpen
  } = useModalManager<'HOGE2' | 'FUGA2'>()

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

変遷(初期)

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

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側で呼び出す時に、

{isModalOpen('hoge') && (
  // modalの中身
)}

のようにkeyを引数として与える際、
コードエディタがサジェストをしてくれない課題に直面しました。
サジェストがないと、タイポでしょうもないエラーとか起こしかねないのでそれだけは避けたい…
ということで次の形に変更しました。

サジェストを出したくて(中期)

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

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') などの引数を記述する際に3種類のサジェストが出るように。
こうなればコーディングの時にタイポを気にせずTabキーで選択すればいいのでストレスが激減しました。

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

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

そして最終形へ

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

(↓ 再掲)

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 }
}

元々以下のようなコードがあったとして、
(注:サンプルなので実際に動かしてはないです。雰囲気だけ掴んでもらえたら幸いです。)

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)} />
      )}
    </>
  )
}

これに対して今回作ったカスタムフックを適用すると、

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の値を他に影響させることなく自由に変更することができるのでいい感じになったような気がします。

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?