0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

問題

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

今回やりたかったこととしては下記になります。

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

解決方法

以下のように、useModal の内部で「モーダルの種類+それに渡す props」をまとめて管理し、openModal 時にコールバックを渡せるようにすると、Page → Hook → Modal コンポーネントという流れで、できます。

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>
  );
};

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を使わない方がいいですが、今回は参考なので使っています。

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>
  );
}

肝はここですね

 <button
    className="btn btn-secondary"
    onClick={() =>
        openModal('addFolder', { onClickAddFolder: handleAddFolder })
    }
 >

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

なぜそれが実現できたのかというと、

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

<ModalRenderer />

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

おわりに

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

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

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?