mott
@mott

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

関数をグローバルなステートで管理できる?(React18+TypeScript+MUI)

解決したいこと

アラートダイアログをuseContextを用いて、共通化を試みる。
ダイアログを表示させる呼び出し元から、アラートダイアログのOKを押下した時の処理を渡す手段はあるのか。

現状の実装

以下の削除処理や保存処理で確認メッセージ(アラートダイアログ)を表示させている。
アラートダイアログの表示内容やボタン押下時の処理を、propsで渡してアラートダイアログを共通化している。
ダイアログの開閉ステートは、useContextでグローバル化している。

(1)保存処理
保存ページの「保存」ボタンを押下→【保存しますか?】のアラートダイアログを表示→【削除】ボタン押下で【保存処理】実行

(2)削除処理
削除ページの「削除」ボタンを押下→【削除しますか?】のアラートダイアログを表示→【保存】ボタン押下で【削除処理】実行

App.tsx(react-routerでルーティング)
import React from "react";

import { Route, BrowserRouter as Router, Routes } from "react-router-dom";
// ------- 中略 -------//

function App() {
  return (
    <AppContextProvider>
        <Router basename="/">
            <Routes>
                <Route path="/" element={<Layout />}>
                    <Route path="/save" element={<SavePage />} />
                    <Route path="/delete" element={<DeletePage />} />
                </Route>
            </Routes>
       </Router>
    </AppContextProvider>
  );
}
Layout.tsx(各ページをラップする)
import React from "react";

import { Outlet } from "react-router-dom";
// ------- 中略 -------//

function Layout() {
  return (
    <Box>
        <Outlet />
    </Box>
  );
}
SavePage.tsx(保存を行うページ)※一部抜粋
function SavePage(){
  // コンテキストからグローバルな値を取得
  const { setAlertOpen } = useAppContext();
  // 「保存」ボタン押下時処理(アラートダイアログを表示する)
  const handleClickSave = () => {
  	setAlertOpen(true);
  }
  const handleSave = () => {
  	//保存処理
  }
  return(
    <Box>
      <Box>
        {form}  //保存するデータのフォーム
        <Button onClick={handleClickSave}>
          保存
        </Button>
      </Box>
      <AlertDialog
        title="保存"
        content="保存しますか?"
        btnText="保存"
        handleClickAgree={handleSave} 
        />
    </Box>
  );
}
DeletePage.tsx(削除を行うページ)※一部抜粋
function DeletePage(){
  // コンテキストからグローバルな値を取得
  const { setAlertOpen } = useAppContext();
  // 「削除」ボタン押下時処理(アラートダイアログを表示する)
  const handleClickDelete = () => {
  	setAlertOpen(true);
  }
  const handleDelete = () => {
  	//保存処理
  }
  return(
    <Box>
      <Box>
        {form}  //削除するデータのフォーム
        <Button onClick={handleClickDelete}>
          削除
        </Button>
      </Box>
      <AlertDialog
        title="削除"
        content="削除しますか?"
        btnText="削除"
        handleClickAgree={handleDelete} 
        />
    </Box>
  );
}
AlertDialog.tsx(アラートダイアログ)
import * as React from "react";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";

// ------- 中略 ------- //

interface AlertDialogProps {
  title: string;
  content: string;
  btnText: string;
  handleClickAgree: () => void;
}

export default function AlertDialog({
  title,
  content,
  btnText,
  handleClickAgree,
}: AlertDialogProps) {
  const { alertOpen, setAlertOpen } = useAppContext();

  // アラートダイアログを閉じる処理
  const handleClose = () => {
    setAlertOpen(false);
  };

  return (
    <React.Fragment>
      <Dialog
        open={alertOpen}
        onClose={handleClose}
      >
        <DialogTitle>{title}</DialogTitle>
        <DialogContent>
          <DialogContentText>
            {content}
          </DialogContentText>
        </DialogContent>
        <DialogActions>
          <Button onClick={handleClickAgree} autoFocus>
            {btnText}
          </Button>
          <Button onClick={handleClose}>キャンセル</Button>
        </DialogActions>
      </Dialog>
    </React.Fragment>
  );
}
AppContext.tsx(アラートダイアログの開閉状態をグローバルに管理)
import { createContext, useContext, useState } from "react";

interface AppContextType {
  alertOpen: boolean;
  setAlertOpen: React.Dispatch<React.SetStateAction<boolean>>;
}

export const AppContext = createContext<AppContextType | undefined>(undefined);

export const AppContextProvider = ({ children }: { children: ReactNode }) => {
  // アラートの表示状態を管理するstate
  const [alertOpen, setAlertOpen] = useState(false);
  
  return (
    <AppContext.Provider
      value={{
        alertOpen,
        setAlertOpen,
      }}
    >
      {children}
    </AppContext.Provider>
  );
};

export const useAppContext = () => {
  const context = useContext(AppContext);
  return context;
};

解決したい課題

各ページにAlertDialogコンポーネントを実装している状態。
各ページに1メッセージのダイアログであれば、この実装でも耐えられるが、別内容のダイアログを表示することがこの実装では不可。また、各ページにコンポーネントを呼んでいるため、可読性・メンテナンス性が難あり。各ページをラップしているLayout.tsxでAlertDialogコンポーネントを実装した方がよい。

試したこと

アラートダイアログに渡しているpropsをすべてuseAppContextでグローバルに管理する。

      <AlertDialog
        title="削除"
        content="削除しますか?"
        btnText="削除"
        handleClickAgree={handleDelete} 
        />

propsで渡している以下4つの項目について、AppContext.tsxでグローバルに管理してみる。

  • title:アラートダイアログのタイトル(string)
  • content:アラートダイアログに表示する内容(string)
  • btnText:アラートダイアログのAgreeボタンのテキスト(string)
  • handleClickAgree:Agreeボタンを押下した時の処理(void?)

上記のステートに値をセットするhandleOpenAlertDialog()をAppContextに追加

AppContext.tsx
  // OK押下時のデフォルトの処理
  const defaultAgreeHandle = () => {
    // アラートを閉じる
    setAlertOpen(false);
  }
  const [alertTitle, setAlertTitle] = useState<string>("");
  const [alertContent, setAlertContent] = useState<string>("");
  const [alertBtnText, setAlertBtnText] = useState<string>("");
  const [onClickAgree, setOnClickAgree] = useState<() => void>(defaultAgreeHandle);

  // AlertDialogを開く
  const handleOpenAlertDialog = (alertTitle: string, alertContent: string, alertBtnText: string, onClickAgree: () => void) => {
    setAlertTitle(alertTitle);
    setAlertContent(alertContent);
    setAlertBtnText(alertBtnText);
    setOnClickAgree(onClickAgree);

    setAlertOpen(true);
  }

呼び出し元からは、以下でアラートを表示させる

  // 「保存」ボタン押下時処理(アラートダイアログを表示する)
  const handleClickSave = () => {
    handleOpenAlertDialog("保存", "保存しますか?", "保存", handleSave);
  }

上から3つは、string型なので、AppContext内のuseStateで管理ができたが、voidの関数は、handleClickAgreeを格納することがうまくできなかった。(関数が動かない)
ステートに関数処理は格納できないのだろうか...それともセットの仕方が違うのか...
もっといい方法があるのかも...?

解決策

調査中。

0

2Answer

・react useReducer&オブジェクトの受け渡し
・オブジェクトの中の関数の受け渡し方法
等で調べると解決策出てくるかもです。
応援してます

(もし個人開発なら、個人的にはReact19をお勧めしたいところではあります。再表示のタイミングでReact18には苦しめられる時が)

1Like

それは本当に解決した方がいいのでしょうか?

個人的にはLayout.tsxに定義された〈AlertDialog /〉の各種propsをContextで変更するメリットは基本的にはないと考えています。
なぜなら、Contextはglobalに使えるので、props.nameやらprops.labelは全部useContextのstate.nameやstate.labelで置き換え可能だからです。
直接とってこれるものをわざわざpropsにする意味がないですよね。
ただ、そうなるとLayout.tsxの中に定義される〈AlertDialog /〉はpropsも何も持たないただのAlertDialogでしかありません。こいつがポツンといても「何かAlertするんだろうな」としか思えず、そいつがどのような挙動を取るかは、結局layout.tsxでwrapしているコンポーネントの中身のハンドラに定義されているcontext Action(長い!)を見ないとわかりません。
イベントが発火した時にcontextをdispatchするためです。
これって本当に可読性が高い状態でしょうか?

また、そのように共通化されたコンポーネントはテストが難しいです。
これは推測ですが、おそらく「○○を削除」ボタンを開いた時には、○○削除ダイアログが開かれることが期待されるかと思います。
このテストを書くために、Layout.test.tsxのrenderコンポーネントを〈AppProvider〉でwrapしないといけないのは効率が悪いですし、Dialogが開かれて欲しいのに、テストする内容が「Contextが変更されるかどうか」になるのはいかがなものかと思います。
テストがしづらい設計は良くない設計だと思うので、本当にContextに全てをまとめる必要は、個人的にはないと思います。
AlertDialogにpropsをそのまま渡す今の設計の方が、結果的にメンテナンスが楽になると思いますよ。

0Like

Your answer might help someone💌