はじめに
一つの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の値を他に影響させることなく自由に変更することができるのでいい感じになったような気がします。
## 終わりに
ジェネリクスが苦手でまだまだ自由に使いこなせないのですが、一つずつ試しに書いてみて理解を深められたらいいなと思っています。