Javascriptのalertを自前のalertに置き換えたら後続処理が動いて苦労した話

javascriptのalertを使ってエラー通知や確認などを行っていたが開発も終盤に差し掛かったころ、アプリが利用している専用ブラウザが出力するalertではボタンに初期フォーカスが当たっていないことが分かったため、自前でalertを作ることになった。開発終盤ということもあり、Javascriptのalertと同じ実装になるようにIFを考えていたが、Javascriptのalertはダイアログを終了するまで処理を待機するのでalertを置き換えに失敗してバグが起きないように同じように待機させたい。

  const handleClick = () => {
    alert("open");
    console.log("closed");
  }

ここまで前置き。


検証用にchakra-uiでサクッとモーダルダイアログを作った。 propsはuseDisclosureが提供しているisOpenとonCloseを受け取るようにする。

export const Alert: FC<{isOpen:boolean,onClose:()=>void}> = ({ isOpen,onClose }) => {
    return (
        <Box>
            <Modal isOpen={isOpen} onClose={onClose}>
                <ModalOverlay backgroundColor={"gray"} />
                <ModalContent  width={"10rem"} 
                    height={"5rem"} 
                    border="1px solid"  
                    backgroundColor={"white"}
                    margin="auto"
                    marginTop="0"
                    >
                <ModalBody margin="auto">
                    Open
                </ModalBody>
                </ModalContent>
            </Modal>
        </Box>
      )
}

次にダイアログを起動するためのhooksを作成

戻り値はopen関数とコンポーネントのprops

export const useAlert = ()=>{
  const {isOpen,onOpen,onClose} = useDisclosure();
  const alertOpen = () => {
    onOpen();
  }
  return {
    AlertProps:{
      isOpen,
      onClose
    },
    alertOpen
  }
}

とりあえずダイアログが起動できるようになった。

  const {AlertProps,alertOpen} = useAlert();

  const handleClick = () => {
    alertOpen();
    console.log("closed");
  }
  return (
      <Button value={"Dialog Open"} onClick={handleClick} width="5rem" height={"1.5rem"} />
      <Alert {...AlertProps} />
  );

ここまで事前準備。これからダイアログが閉じた後にconsoleにlogが出力できるようにしたい。


結論から言うと完全にJavascriptのalertと同じような方法で処理を待機させるのはできなかった。

一括置換で対応するという観点で考えるとpromiseを使うのは一考の余地はありかなという感じ。

以下Promiseを使った方法。 hooksのopen関数からPromiseを返し、画面が閉じられるまでsetIntervalでresolveを待機する。 Promise内部ではuseEffectなどのhooksが使えないためPromise内部ではisOpenの値が変わらなくなってしまう。 そのためisOpenを一度refで受ける必要があった。

  const refOpen = useRef<Boolean>(false);
  useEffect(()=>{
    refOpen.current = isOpen;
  },[isOpen])

  const alertOpen = async () => {
    return new Promise<void>((resolve,reject)=>{
      onOpen();
      const set_interval_id = setInterval(()=>{
        if(!refOpen.current) { 
          resolve();
          clearInterval(set_interval_id);
        }
        console.log("waiting...");
      },100);  
    });
  }

利用側は以下の通り。 alert()をawait alertOpen()で置換してやればとりあえず処理フローが変わらずに自前ダイアログに置き換えることはできる。 ただし関数そのものがasyncになってしまうので別の問題を引き起こしてしまう可能性がある。

  const handleClick = async () => {
    await alertOpen();
    console.log("closed");
  }

あきらめてダイアログが閉じたあとの処理をcallbackで受け取るようにするのがよさげという結論

  const handleClick = () => {
    alertOpen(()=>{
      console.log("closed");
    });
  }