FE開発においてモーダル実装はあらゆるところで使う場面があり、要件やモーダルの数が増えるに従って実装も複雑になり設計に悩まされてる方も多いと思います。
そこでモーダル実装に関するベストプラクティスを考えてみました
設計方針
状態管理にRecoil、モーダルに関するロジックをカスタムhook化することにしました
Recoilを使う理由は、Recoilに用意されているステート管理のatomFamily
がモーダル実装と非常に相性がいいからです
モーダル実装のアンチパターン
const MainPage = () => {
const [isFaqVisible, setIsFaqVisible] = useState(false)
return (
<div>
{isFaqVisible && <FaqModal />}
<button onClick={() => setIsFaqVisible(true)}>
クリックするとFAQモーダルが開くよ
</button>
</div>
)
}
export default MainPage
あるあるな書き方ですが、このように同じコンポーネント内でモーダルの開閉状態をもつと以下のデメリットが出てしまいます
- 開閉処理と表示の状態が完全にコンポーネントに依存しているため、別のコンポーネントからの開閉処理がやりずらい。特にFAQモーダルなど多くのページからコールされる事が予想されるため、予期せぬバグの温床になりやすい
- 表示させたいモーダルの数が増えるごとにstateが増えてコードの見通しが悪くなり保守性が悪くなる
解決策
- 開閉に関する状態と処理はカスタムhook化してコンポーネントと疎結合にする
- モーダルの状態を
atomFamily
で抽象化する事で、stateの冗長な記述を解消する
モーダルのステート管理
import { atomFamily } from 'recoil'
export type ModalType =
| 'contact'
| 'faq'
| 'confirm'
export const ModalVisibilityState = atomFamily({
key: 'ModalVisibilityState',
default: false,
})
ここでは、モーダルの種類に応じたModalType
とatomFamily
でモーダルのステートを抽象化しています
FAQモーダル
import Modal from 'components/Common/Modal'
import useModal from 'hooks/useModal'
const FaqModal = () => {
const [isVisible, setIsVisible] = useModal('faq')
return (
isVisible && (
// propsにモーダルを閉じるcomponent
<Modal closeModal={() => setIsVisible(false)}>{/*Modalの中身*/}</Modal>
)
)
}
export default FaqModal
ポイントはコンポーネントの内部で表示/非表示の関心だけを持たせる事で、呼び出しもとで開閉状態を意識せず実装できる点です
モーダルのカスタムフック
import { useRecoilState, SetterOrUpdater } from 'recoil'
import { ModalVisibilityState, ModalType } from 'states/modal'
type Response = [
boolean,
SetterOrUpdater<boolean>
]
const useModal = (modalType: ModalType): Response => {
const [isVisible, setIsVisible] = useRecoilState(
ModalVisibilityState(modalType)
)
return [isVisible, setIsVisible]
}
export default useModal
atomFamily
で作ったModalVisibilityState
をuseRecoilState
でラップして一意なステートを作成して開閉処理の管理をしています
こうすることで、モーダル毎に応じたstateを一々記述する必要がなくなりスッキリかけます
また、引数にModalType
を渡すことで、どのコンポーネントからも柔軟に開けるようになります
モーダルを表示させたいコンポーネント
import React from 'react'
import FaqModal from 'components/Modal/FaqModal'
const DisplayModalComponent = () => {
return (
<FaqModal />
)
}
export default DisplayModalComponent
モーダルを呼び出したいコンポーネント
import useModal from 'hooks/useModal'
const CallModalComponent = () => {
const [, setIsVisible] = useModal("faq")
return (
<button onClick={() => setIsVisible(true)}>
クリックするとFAQモーダルが開くよ
</button>
)
}
export default CallModalComponent
useModal
を呼び出して、ボタンをクリックするとFAQモーダルが開くようになります
考察
他にもcontextやRecoilRootを使ってグローバルに状態を管理する方法もあると思いますが、この書き方の方が遥かにスッキリ描けるので今のところ個人的なベストプラクティスかなと思います!