Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clearing multiple timeouts in useEffect

Using React for practice, I'm trying to build a small notification system that disappears after a given period using timeouts.

A sample scenario;

  1. User creates one notification and wait for it's to expire
  2. The cleanup function runs from useEffect and clears the timeout.

This would be no problem and clears out the only available timeout. The problem appears when I'm adding more:

  1. Render #1 - adding the first notification
  2. Render #2 - the cleanup function calls from render #1 for adding a new notification. This adds a new notification but clears a timeout before it's done.
  3. The timeout expires from render #2 so runs the cleanup function and clears the right timeout.

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 }
}
like image 502
Bas Avatar asked Oct 30 '25 09:10

Bas


1 Answers

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

Edit clearing-multiple-timeouts-in-useeffect

like image 162
Drew Reese Avatar answered Oct 31 '25 23:10

Drew Reese



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!