UGA Boxxx

つぶやきの延長のつもりで、知ったこと思ったこと書いてます

【React】クリックするたびに生み出される通知コンポーネントをつくる

And Designの通知コンポーネントのような、クリックするたびに生み出される通知コンポーネントをつくりたい

ant.design

大まかな方針としては、クリックするたびにbody要素に通知要素がappendされていき、時間が経つとその要素が消えるというような実装をしようと思う

bodyに通知要素をappendするのはポータルを使う

uga-box.hatenablog.com

表示/非表示はタスクキューを使って、非同期で行うようにする

つまり、クリックしたらそのイベントハンドラの中で「表示」、時間が来たらそのsetTimeoutの中で「非表示」ではなく、クリックしたら「表示するタスク」をキューにセット、時間が来たら「非表示にするタスク」をキューにセットするような仕組みで考える

まずは、通知リストコンポーネントの中で空の通知リストを用意し、「表示するタスク」が来たら通知リストに通知コンポーネントを追加、「非表示にするタスク」が来たら通知リストから通知コンポーネントを削除するような関数を定義する

  const [notificationList, setNotificationList] = useState<Notification[]>([]);
  ...
  useImperativeHandle(ref, () => ({
    open: (notificationProps) => {
      setNotificationList((list) => {
        let clone = [...list];

        const index = clone.findIndex(
          (item) => item.id === notificationProps.id,
        );
        if (index >= 0) {
          clone[index] = notificationProps;
        } else {
          clone.push(notificationProps);
        }

        return clone;
      });
    },
    close: (key) => {
      onNoticeClose(key);
    },
    destroy: () => {
      setNotificationList([]);
    },
  }));
...
  return createPortal(
    <>
      {notificationList.map((notification, index) => (
        <Wrapper key={notification.id} index={index}>
          <Notice onClose={handleCloseNotification} {...notification} />
        </Wrapper>
      ))}
    </>,
    document.body,
  );

このとき、通知リストコンポーネントは非制御コンポーネントとして定義するため、useImperativeHandleを使って上記2つの関数が呼び出し側で使えるようにする

ja.reactjs.org

通知リストコンポーネントの呼び出しはuseNotificationというカスタムフック内で行う

  const [taskQueue, setTaskQueue] = useState<Task[]>([]);
  ...
  const handleCloseNotification = (id: string) => {
    setTaskQueue((queue) => [...queue, { type: "close", id }]);
  };
  const handleOpenNotification = (notification: Notification) => {
    if (!notification.id) {
      notification.id = `notification-${uniqueKey}`;
      uniqueKey += 1;
    }
    setTaskQueue((queue) => [...queue, { type: "open", notification }]);
  };
  useEffect(() => {
    // Flush task when node ready
    if (notificationsRef.current && taskQueue.length) {
      taskQueue.forEach((task) => {
        switch (task.type) {
          case "open":
            notificationsRef.current?.open(task.notification);
            break;

          case "close":
            notificationsRef.current?.close(task.id);
            break;
        }
      });

      setTaskQueue([]);
    }
  }, [taskQueue]);
...

ここでは、空のタスクキューリストを用意しておき、「表示するタスク」をキューリストに追加する関数(handleOpenNotification)と「非表示にするタスク」をキューリストから除外する関数(handleCloseNotification)を定義して、どちらかによってタスクキューリストが更新されたらタスクリストの中身が実行されるようuseEffectを定義しておく

あとは、handleOpenNotificationhandleCloseNotificationを外部に公開して使われるのを待つ

このときhandleOpenNotificationは任意の場所で使われればよいが、

handleCloseNotificationは通知リストコンポーネントで呼び出している通知コンポーネント内のsetTimeout内で呼ぶようにすることで、時間が来たら消える仕組みが実現できる

  const close = () => {
    props.onClose(props.id);
  };
...
  useEffect(() => {
    const timeout = setTimeout(() => {
      close();
    }, 3000);
    return () => {
      clearTimeout(timeout);
    };
  }, []);

これでやりたいことができた