🧐

モーダルの出し分け方法を色々考えてみる

2024/01/04に公開2

はじめに

モーダルを作成する機会があったのですが、モーダル内コンテンツの出し分けにかなりハマりました。
半分メモ書きみたいになってますが一応備忘録として残しておきます。
なお出し分ける部分だけに着目しているため、CSSやストアでの状態管理・アクセシビリティについては全く考慮しておりません。

やりたいこと

Modal01のボタンを押したらModal01コンポーネントのコンテンツが表示される
Modal02のボタンを押したらModal02コンポーネントのコンテンツが表示される

結論

色々試した結果、結局これが一番シンプルに書けそうでした。
ただ引数でコンポーネントを渡すのがなんとも気持ち悪かったので、他の方法でできないか色々試してみました。

function App() {
  const [isOpen, setIsOpen] = useState(false);
  const [openModalComp, setOpenModalComp] = useState<JSX.Element>();

  const open = (props: JSX.Element): void => {
    setIsOpen(true);
    setOpenModalComp(props);
  };

  return (
    <>
      <div>
        <button onClick={() => open(<Modal01 />)}>Modal01</button>
        <button onClick={() => open(<Modal02 />)}>Modal02</button>
      </div>
      {isOpen && <Modal>{openModalComp}</Modal>}
    </>
  );
}

試したけどうまくいかなかった方法

押したボタンのテキストを取得し、createElement()で表示する

buttonのテキストを取得し、そのテキストをcreateElement()の第一引数(タグ名)として使えないかと思った
**
なぜうまくいかなかったか

  • ボタンが複数あった場合にテキストの取得方法がわからない
const App = () => {
 const [isOpen, setIsOpen] = useState(false);
 const [openModalComp, setOpenModalComp] = useState("Modal01");

 const open = (): void => {
   setIsOpen(true);
 };

 return (
   <>
     <div>
       <button onClick={() => open()}>{openModalComp}</button>
       {/* Modal02のテキストどう取得する? */}
       <button onClick={() => open()}>Modal02</button>
     </div>
     {isOpen && createElement(Modal01, null, "モーダル1です")}
   </>
 );
};
  • createElement()の第一引数には関数そのものを入れなければならず、stringや関数の戻り値を入れることはできないっぽい(useStateを挟むと関数が実行されてしまい、戻り値が入ってしまう)
const App = () => {
 const [isOpen, setIsOpen] = useState(false);
 const [openModalComp, setOpenModalComp] = useState();

 const open = (component): void => {
   setIsOpen(true);
   setOpenModalComp(component);
 };

 return (
   <>
     <div>
       <button onClick={() => open(Modal01)}>Modal01</button>
       <button onClick={() => open(Modal02)}>Modal02</button>
     </div>
     
     {/* これは正常に表示される */}
     {isOpen && createElement(Modal01, null, "モーダル1です")}
     {/* これはエラーになる */}
     {isOpen && createElement(openModalComp, null, "モーダル1です")}
   </>
 );
};

↓これでもダメでした

const App = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [openModalComp, setOpenModalComp] = useState("");

  const comp = {
    Modal01: Modal01,
    Modal02: Modal02,
  };

  const open = (component: string): void => {
    setIsOpen(true);
    setOpenModalComp(comp[component]);
  };

  return (
    <>
      <div>
        <button onClick={() => open("Modal01")}>Modal01</button>
        <button onClick={() => open("Modal02")}>Modal02</button>
      </div>
      {isOpen && createElement(openModalComp, null, "hello")}
    </>
  );
};

console.log(openModalComp)の結果
関数が実行され戻り値が返されており、createElementで使えない

console.log(Modal01)の結果
関数は実行されず関数そのものが返され、createElementで使える


asプロパティでタグ名を書き換える

以下を参考にasプロパティでタグ名を書き換えようと思った
https://zenn.dev/yahsan2/articles/20220118-987ddf760fbdb2

なぜうまくいかなかったか

  • asはdivやpなどの既定のタグのみしか受け付けないっぽく、コンポーネント名を入れることができなかった

  • ↓この方法でもやはりpropsで渡される値がuseStateを通っているとエラーになる

interface Props2 {
  tag?: React.ElementType;
  children?: string;
}

const Main2 = (props: JSX.Element) => (
  <>
    <Example2 tag={props.props}></Example2>
  </>
);

export const Example2 = ({ tag: Tag = "div", children }: Props2) => (
  <Tag>{children}</Tag>
);

コメントお待ちしてます

初学者かつメモ書き状態のためわかりにくいところ多々あるかと思いますが、もしここが違っている、こうすればできるなどあればコメントいただけるとありがたいです。

参考文献

https://zenn.dev/yahsan2/articles/20220118-987ddf760fbdb2
https://de-milestones.com/react-component-props/
https://react.dev/reference/react/createElement
https://www.web-dev-qa-db-ja.com/ja/reactjs/反応コンポーネントに要素を動的に作成できません/838894689/
https://qiita.com/NeGI1009/items/d38e517f1647ff2000c7

Discussion

Honey32Honey32

失礼します。

ステートは Single Source of Truth (信頼できるただひとつの情報源) として扱えるので一つにまとめられますが、(いや、でも面倒くさいなら isModal01Open isModal02Modal の2つのステートに分けるだけで十分です)

modalState に基づいて JSX をレンダリングする場では「共通化をしない」ほうが便利です。Modal01, 02 などの仕様がいくら複雑になっても対応できます。

// ステートは代数的データ型(判別可能なユニオン型)を使います
type ModalState = 
  | { type: "modal01", payload: { hoge: string }}
  | { type: "modal02" }
  | undefined; // 全て閉じた状態

function App() {
  const [modalState, setModalState] = useState<ModalState>(undefined);

  const handleOpenModal01 = () => {
    setModalState({ type: "modal01" });
  }

  const handleOpenModal02 = () => {
    setModalState({ type: "modal02" });
  }

  // JSX の中に if が書けないのでここに書きます
  const renderModal = () => {
    if (!modalState) return null;
    switch(modalState.type) {
      case "modal01": {
        // 共通化していないので、Modal に渡す Props が必要になっても対応可能
        const { hoge } = modalState.payload;
        return (
          <Modal>
            <Modal01 hoge={hoge} />
          </Modal>
        );
      }
      case "modal02": {
        return (
          <Modal>
            <Modal02 />
          </Modal>
        );
      }
    }
  }

  return (
    <>
      {renderModal()}
      <div>
        <button onClick={handleOpenModal01}>Modal01</button>
        <button onClick={handleOpenModal02}>Modal02</button>
      </div>
    </>
  );
};
aiwonaiwon

コメントありがとうございます、本当にありがたいです!試してみます!