Clean up multiple timeouts from React provider

19 Views Asked by At

I have the following React provider:

import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';

export const AlertContext = createContext({
  addAlert: () => {},
  alerts: [],
  clearAlert: () => {},
  setAlerts: () => {},
});

export function useAlert() {
  return useContext(AlertContext);
}

export function AlertProvider(props: { children: React.ReactNode }) {
  const [alerts, setAlerts] = useState([]);

  const clearAlert = useCallback(
    (idToRemove: string | null) => {
      // Clear a specific alert by ID.
      if (idToRemove) {
        setAlerts(alerts => alerts.filter(a => a.id !== idToRemove));
        return;
      }
    },
    [],
  );

  const addAlert = useCallback(
    (alert: any, options?: { autoDismissTime?: number }) => {
      setAlerts(alerts => {
  
        if (options?.autoDismissTime) {
          setTimeout(() => {
            clearAlert(alert.id);
          }, options.autoDismissTime)
        }

        return [...alerts, alert];
      });
    },
    [clearAlert],
  );

  return (
    <AlertContext.Provider value={{ alerts, setAlerts, addAlert, clearAlert }}>
      {props.children}
      <ClientOnly>
        {() => <AlertContainer alerts={alerts} clearAlert={clearAlert} />}
      </ClientOnly>
    </AlertContext.Provider>
  );
}

And then in one of my components I do this:

const { addAlert } = useAlert();
// Add the alert
addAlert(...)

I would like to know what is the proper way to ensure that I don't leave any timeouts hanging in that provider (how should I manage the clearTimeout). I have seen examples on how to unset a specific timer in a component when it unmounts, but in this case I can have multiple timeouts (as many as alerts are being set with an autoDismissTime)

1

There are 1 best solutions below

4
Konrad On BEST ANSWER

At the start of the component:

const timeoutsRef = useRef<Record<number, number>>([])

Then:

const id = setTimeout(() => {
  clearAlert(alert.id);
  delete timeoutsRef.current[alert.id]
}, options.autoDismissTime)
timeoutsRef.current[alert.id] = id

And then cleanup:

useEffect(() => {
  return () => {
    Object.values(timeoutsRef.current).forEach((id) => {
      clearTimeout(id)
    })
  }
}, [])