And Designの通知コンポーネントのような、クリックするたびに生み出される通知コンポーネントをつくりたい
大まかな方針としては、クリックするたびにbody要素に通知要素がappendされていき、時間が経つとその要素が消えるというような実装をしようと思う
bodyに通知要素をappendするのはポータルを使う
表示/非表示はタスクキューを使って、非同期で行うようにする
つまり、クリックしたらそのイベントハンドラの中で「表示」、時間が来たらその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つの関数が呼び出し側で使えるようにする
通知リストコンポーネントの呼び出しは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を定義しておく
あとは、handleOpenNotification
とhandleCloseNotification
を外部に公開して使われるのを待つ
このときhandleOpenNotification
は任意の場所で使われればよいが、
handleCloseNotification
は通知リストコンポーネントで呼び出している通知コンポーネント内のsetTimeout内で呼ぶようにすることで、時間が来たら消える仕組みが実現できる
const close = () => { props.onClose(props.id); }; ... useEffect(() => { const timeout = setTimeout(() => { close(); }, 3000); return () => { clearTimeout(timeout); }; }, []);
これでやりたいことができた