Using React for practice, I'm trying to build a small notification system that disappears after a given period using timeouts.
A sample scenario;
This would be no problem and clears out the only available timeout. The problem appears when I'm adding more:
It's a fairly simple component, which renders a array of objects (with the timeout in it) from a Zustand store.
export const Notifications = () => { const { queue, } = useStore()
useEffect(() => {
if (!queue.length || !queue) return
// eslint-disable-next-line consistent-return
return () => {
const { timeout } = queue[0]
timeout && clearTimeout(timeout)
} }, [queue])
return (
<div className="absolute bottom-0 mb-8 space-y-3">
{queue.map(({ id, value }) => (
<NotificationComponent key={id} requestDiscard={() => id}>
{value}
</NotificationComponent>
))}
</div> ) }
My question is; is there any way to not delete a running timeout when adding a new notification? I also tried finding the last notification in the array by queue[queue.length - 1], but it somehow doesn't make any sense
My zustand store:
interface State {
queue: Notification[]
add: (notification: Notification) => void
rm: (id: string) => void
}
const useNotificationStore = c<State>(set => ({
add: (notification: Notification) =>
set(({ queue }) => ({ queue: [...queue, notification] })),
rm: (id: string) =>
set(({ queue }) => ({
queue: queue.filter(n => id !== n.id),
})),
queue: [],
}))
My hook for adding notifications;
export function useStoreForStackOverflow() {
const { add, rm } = useNotificationStore()
const notificate = (value: string) => {
const id = nanoid()
const timeout = setTimeout(() => rm(id), 2000)
return add({ id, value, timeout })
}
return { notificate }
}
I think with a minor tweak/refactor you can instead use an array of queued timeouts. Don't use the useEffect hook to manage the timeouts other than using a single mounting useEffect to return an unmounting cleanup function to clear out any remaining running timeouts when the component unmounts.
Use enqueue and dequeue functions to start a timeout and enqueue a stored payload and timer id, to be cleared by the dequeue function.
const [timerQueue, setTimerQueue] = useState([]);
useEffect(() => {
return () => timerQueue.forEach(({ timerId }) => clearTimeout(timerId));
}, []);
const enqueue = (id) => {
const timerId = setTimeout(dequeue, 3000, id);
setTimerQueue((queue) =>
queue.concat({
id,
timerId
})
);
};
const dequeue = (id) =>
setTimerQueue((queue) => queue.filter((el) => el.id !== id));
Demo
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With