【Next.js/daisy UI】モーダルの種類とそれに渡すpropsをまとめて管理する方法

# はじめに
モーダルを作っていた際に、困ったことがあったので、本記事にまとめます

# 問題
モーダル内でコンポーネントを分けて作成していた際に、
各モーダルの中身と操作がそれぞれ違うため、一括管理するのに何かいい方向性はないかと考えていました。


今回やりたかったこととしては下記になります。
- モーダルそのもののコンポーネントをフック内で挟んで、`page.tsx`などもonClickなどで簡易的に呼び出せるようにする
今回は、例として`ModalAddFolder`コンポーネント一つだけにします
- 高レイヤー(今回は`page.tsx`です)からモーダルのコンポーネントにpropsとして渡すときにモーダル内の処理も渡せるようにする


# 解決方法
以下のように、useModal の内部で「モーダルの種類＋それに渡す props」をまとめて管理し、openModal 時にコールバックを渡せるようにすると、Page → Hook → Modal コンポーネントという流れで、できます。
``` typescript:ModalAddFolder.tsx
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>
  );
};

```

``` typescript:useModal.tsx
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`を使わない方がいいですが、今回は参考なので使っています。
``` typescript:page.tsx
'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>
  );
}

```

肝はここですね
```typescript
 <button
    className="btn btn-secondary"
    onClick={() =>
        openModal('addFolder', { onClickAddFolder: handleAddFolder })
    }
 >
```

ここでは、モーダルをopenModalで呼び出しているんですが、
そのモーダル内にあるpropsの関数(`onClickAddFolder`)に`page.tsx`で作った`handleAddFolder`を渡すことができました。

なぜそれが実現できたのかというと、
```typescript:useModal.tsx

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]`この中でこういうふうに呼ぶよって定義してあげるだけで十分ってことですね


```typescript
<ModalRenderer />
```
だけでモーダルのコンポーネントに処理をわざわざ書く必要もないので楽になりましたね。

# おわりに
ここ一番でハマりましたが、理解できたので、
こういった書き方は本当に学びになりました。

次は、モーダル内のinputにあるvalueとpropsに関することを書きたいなと思います