110
80

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Reactでモーダル実装のベストプラクティスを考えてみた

Last updated at Posted at 2022-08-16

FE開発においてモーダル実装はあらゆるところで使う場面があり、要件やモーダルの数が増えるに従って実装も複雑になり設計に悩まされてる方も多いと思います。
そこでモーダル実装に関するベストプラクティスを考えてみました

設計方針

状態管理にRecoil、モーダルに関するロジックをカスタムhook化することにしました
Recoilを使う理由は、Recoilに用意されているステート管理のatomFamilyがモーダル実装と非常に相性がいいからです

モーダル実装のアンチパターン

MainPage.tsx
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の冗長な記述を解消する

モーダルのステート管理

src/states/modal.ts
import { atomFamily } from 'recoil'

export type ModalType =
  | 'contact'
  | 'faq'
  | 'confirm'
export const ModalVisibilityState = atomFamily({
  key: 'ModalVisibilityState',
  default: false,
})

ここでは、モーダルの種類に応じたModalTypeatomFamilyでモーダルのステートを抽象化しています

FAQモーダル

Faq.tsx
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

ポイントはコンポーネントの内部で表示/非表示の関心だけを持たせる事で、呼び出しもとで開閉状態を意識せず実装できる点です

モーダルのカスタムフック

src/hooks/useModal.ts
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で作ったModalVisibilityStateuseRecoilStateでラップして一意なステートを作成して開閉処理の管理をしています
こうすることで、モーダル毎に応じたstateを一々記述する必要がなくなりスッキリかけます
また、引数にModalTypeを渡すことで、どのコンポーネントからも柔軟に開けるようになります

モーダルを表示させたいコンポーネント

DisplayModalComponent.tsx
import React from 'react'
import FaqModal from 'components/Modal/FaqModal'

const DisplayModalComponent = () => {
  return (
   <FaqModal />
  )
}

export default DisplayModalComponent

モーダルを呼び出したいコンポーネント

CallModalComponent.tsx
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を使ってグローバルに状態を管理する方法もあると思いますが、この書き方の方が遥かにスッキリ描けるので今のところ個人的なベストプラクティスかなと思います!

110
80
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
110
80

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?