はじめに
モーダルを作っていた際に、困ったことがあったので、本記事にまとめます
問題
モーダル内でコンポーネントを分けて作成していた際に、
各モーダルの中身と操作がそれぞれ違うため、一括管理するのに何かいい方向性はないかと考えていました。
今回やりたかったこととしては下記になります。
- モーダルそのもののコンポーネントをフック内で挟んで、
page.tsx
などもonClickなどで簡易的に呼び出せるようにする
今回は、例としてModalAddFolder
コンポーネント一つだけにします - 高レイヤー(今回は
page.tsx
です)からモーダルのコンポーネントにpropsとして渡すときにモーダル内の処理も渡せるようにする
解決方法
以下のように、useModal の内部で「モーダルの種類+それに渡す props」をまとめて管理し、openModal 時にコールバックを渡せるようにすると、Page → Hook → Modal コンポーネントという流れで、できます。
import React, { useState } from 'react';
import { Button } from './Button';
import { ModalBase } from './ModalBase';
type Props = {
onClose: () => void;
onSubmit: (name: string) => Promise<void>;
};
export const ModalAddFolder: React.FC<Props> = ({ onClose, onSubmit }) => {
const [name, setName] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
try {
setLoading(true);
await onSubmit(name);
onClose();
} catch (e) {
console.error(e);
// エラーハンドリング(トースト表示など)
} finally {
setLoading(false);
}
};
return (
<ModalBase title="フォルダを追加" description="フォルダを追加してください">
<input
className="input w-full mb-4"
value={name}
onChange={e => setName(e.target.value)}
placeholder="フォルダ名"
/>
<div className="flex items-center gap-2">
<Button text="キャンセル" className="btn-ghost" onClickButton={onClose} />
<Button
text="追加"
className="btn-primary"
onClickButton={handleSubmit}
disabled={loading || !name.trim()}
/>
</div>
</ModalBase>
);
};
import { useState, useCallback, ReactNode } from 'react';
import { ModalAddFolder } from '@/app/components/blocks/ModalAddFolder';
export type ModalType = 'addFolder'; // 将来的に 'delete' などを追加
// モーダルに渡す props を型定義
type ModalPayloads = {
addFolder: {
onClickAddFolder: (name: string) => Promise<void>;
};
};
export function useModal() {
// { type, props } または null
const [modalState, setModalState] = useState<
{ type: ModalType; props: ModalPayloads[ModalType] } | null
>(null);
// props込みで開く
const openModal = useCallback(
<T extends ModalType>(
type: T,
props: ModalPayloads[T]
) => {
setModalState({ type, props });
},
[]
);
const closeModal = useCallback(() => {
setModalState(null);
}, []);
// レンダラーコンポーネント
const ModalRenderer: React.FC = () => {
if (!modalState) return null;
const { type, props } = modalState;
switch (type) {
case 'addFolder':
return (
<ModalAddFolder
onClose={closeModal}
onClickAddFolder={props.onClickAddFolder}
/>
);
// case 'delete': ...
default:
return null;
}
};
return { openModal, closeModal, ModalRenderer };
}
呼び出し方は下記になります。
可能な限り、page.tsx自体はuse client
を使わない方がいいですが、今回は参考なので使っています。
'use client';
import React from 'react';
import { useModal } from '@/app/hooks/useModal';
import { useFolders } from '@/app/hooks/useFolders';
export default function Page() {
const { openModal, ModalRenderer } = useModal();
const { addFolder, fetchFolders } = useFolders();
const handleAddFolder = async (name: string) => {
await addFolder(name);
await fetchFolders();
};
return (
<div className="p-4">
<button
className="btn btn-secondary"
onClick={() =>
openModal('addFolder', { onClickAddFolder: handleAddFolder })
}
>
フォルダ追加
</button>
{/* ここでモーダルをまとめてレンダー */}
<ModalRenderer />
</div>
);
}
肝はここですね
<button
className="btn btn-secondary"
onClick={() =>
openModal('addFolder', { onClickAddFolder: handleAddFolder })
}
>
ここでは、モーダルをopenModalで呼び出しているんですが、
そのモーダル内にあるpropsの関数(onClickAddFolder
)にpage.tsx
で作ったhandleAddFolder
を渡すことができました。
なぜそれが実現できたのかというと、
export type ModalType = 'addFolder'; // 将来的に 'delete' などを追加
// モーダルに渡す props を型定義
type ModalPayloads = {
addFolder: {
onClickAddFolder: (name: string) => Promise<void>;
};
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// { type, props } または null
const [modalState, setModalState] = useState<
{ type: ModalType; props: ModalPayloads[ModalType] } | null
>(null);
// props込みで開く
const openModal = useCallback(
<T extends ModalType>(
type: T,
props: ModalPayloads[T]
) => {
setModalState({ type, props });
},
[]
);
このuseModal.tsx
内にある、ここの部分が大事になってきます。
ModalTypeは今後増えていく機能の名前を任意にして足していくだけでいいですし、
ModalPayloadsは処理そのものの型定義をするだけです。
それを、const [modalState, setModalState]
この中でこういうふうに呼ぶよって定義してあげるだけで十分ってことですね
<ModalRenderer />
だけでモーダルのコンポーネントに処理をわざわざ書く必要もないので楽になりましたね。
おわりに
ここ一番でハマりましたが、理解できたので、
こういった書き方は本当に学びになりました。
次は、モーダル内のinputにあるvalueとpropsに関することを書きたいなと思います