👷
Modalコンポーネントの実装の問題点と解決方法
この記事では、Reactでモーダルコンポーネントを実装する方法について説明します。HTMLのdialogタグなどマークアップには触れません。
前提
以下の共通モーダルコンポーネントを使用することを前提にします。詳細な実装は重要でないため、簡易的な例で説明します。
export const BaseModal = ({visible, children}: {visible: boolean; children: ReactNode}) => {
return <FadeTransition in={visible}>{children}<FadeTransition>
}
FadeTransitionコンポーネントはフェードアニメーションでモーダルを表示するために使用されています。
visible: false
であっても、アニメーションを有効にするためにはコンポーネントがマウントされた状態である必要があります。
特定の機能に依存するモーダルコンポーネント
BaseModalコンポーネントを使用して特定の機能に依存するモーダルコンポーネントの実装方法について考察します。
実装例1
propsにモーダルの開閉フラグ(visible)とコンテンツを表示するために必要な情報(requestParams)を受け取る。
export const SampleModal = ({visible, requestParams}: {visible: boolean; requestParams: { id: string}}) => {
const { data } = useFetchData(requestParams)
return (
<BaseModal visible={visible}>
<div className={
//...
}>
{data && <p>{data.name}</p>}
</div>
<BaseModal>
)
}
問題点
- 親コンポーネントから渡される
requestParams
が未確定の場合、SampleModalコンポーネントをマウントできない。
const REDUCER: Reducer<
{ visible: false } | { visible: true; requestParams: { id: string } },
{}
> = (prevState, action) => {
//...
}
export const Parent = () => {
const [state, dispatchState] = useReducer(REDUCER, { visible: false })
return (
<>
<SampleModal
visible={state.visible}
requestParams={state.requestParams} // state.requestParamsがoptionalなので指定できない
/>
{state.visible && ( // state.visible === falseの場合にSampleModalがマウントされず、FadeTransitionが正常に動かない
<SampleModal
visible={state.visible}
requestParams={state.requestParams}
/>
)}
</>
)
}
実装例2
実装例1の requestParams
を任意にする。
export const SampleModal = ({visible, requestParams}: {visible: boolean; requestParams?: { id: string}}) => {
const { data } = useFetchData(requestParams) // useFetchDataは必須パラメータであるrequestParamsが存在しないと使用することができない
return (
<BaseModal visible={visible}>
<div className={
//...
}>
{data <p>{data.name}</p>}
</div>
<BaseModal>
)
}
改善点
- 実装例1の問題点である
requestParams
が未確定の場合でも、SampleModalコンポーネントをマウントできるようになった。
問題点
-
requestParams
がAPIを呼ぶのに必須のパラメータである場合、useFetchData内で条件分岐が発生することになる。[1] -
requestParams
を受け取れなくなった場合でもコンポーネントが破棄されず、モーダルの再表示時に古い情報が表示される可能性がある。
実装例3
モーダルのラッパーとコンテンツを分離して、親からそれぞれ指定する。
export const Wrapper = ({visible, children}: {visible: boolean; children: ReactNode}) => {
return (
<BaseModal visible={visible}>
<div className={
//...
}>
{children}
</div>
</BaseModal>
)
}
export const Content = ({requestParams}: {requestParams: {id: string}}) => {
const { data } = useFetchData(requestParams)
return <p>{data.name}</p>
}
export const SampleModal = {
Wrapper,
Content
}
export const Parent = () => {
const [state, dispatchState] = useReducer(REDUCER, { visible: false })
return (
<>
<SampleModal.Wrapper visible={state.visible}>
{state.visible && (
<SampleModal.Content requestParams={state.requestParams} />
)}
</SampleModal.Wrapper>
</>
)
}
改善点
- Wrapperコンポーネントは常にマウントされているため、FadeTransitionが正常に動作する。
- Contentコンポーネントは
requestParams
が確定した場合のみマウントされる。 - Contentコンポーネントは
requestParams
を受け取れなくなったら破棄される。 - Contentコンポーネントの
requestParams
は必須項目として扱われるため、コンポーネント内で条件分岐が不要。
問題点
- Wrapperのchildrenの型が制限できない。
- Wrapperのchildrenは1つのContentコンポーネントのみと制限したいができない。
実装例4
Wrapperコンポーネントに直接Contentコンポーネントを配置する。Wrapperコンポーネントでは requestParams
を任意Propとして受け取り、Contentコンポーネントでは必須Propとして受け取る。
ここではWrapperコンポーネントはSampleModalという名前に変えます。
export const SampleModal = ({
visible,
requestParams
}: {
visible: boolean
requestParams?: {id: string} // requestParamsがoptionalなのがポイント
}) => {
return (
<BaseModal visible={visible}>
<div className={
//...
}>
{requestParams && <Content />}
</div>
</BaseModal>
)
}
const Content = ({
requestParams
}: {
requestParams: {id: string} // ContentコンポーネントはrequestParamsを必須項目として受け取ることでContentコンポーネント内で条件分岐不要となる、またrequestParamsが渡せなくなったらコンポーネント毎破棄される
}) => {
const { data } = useFetchData(requestParams)
return <p>{data.name}</p>
}
export const Parent = () => {
const [state, dispatchState] = useReducer(REDUCER, { visible: false })
return (
<>
<SampleModal {...state} />
</>
)
}
改善点
- Wrapperコンポーネントは常にマウントされているため、FadeTransitionが正常に動作する。
- Contentコンポーネントは
requestParams
が確定した場合のみマウントされる。 - Contentコンポーネントは
requestParams
を受け取れなくなったら破棄される。 - Contentコンポーネントの
requestParams
は必須項目として扱われるため、コンポーネント内で条件分岐が不要。 - 実装例3で問題となったWrapperコンポーネントのchildrenの型が制限できない問題が解決された。
まとめ
実装例4がよさそうです(実装例3も悪くないと思います)
Discussion