timolins / react-hot-toast

Smoking Hot React Notifications 🔥
https://react-hot-toast.com
MIT License
9.84k stars 331 forks source link

Add a limit parameter to react-hot-toast #294

Open ivashchenko999 opened 1 year ago

ivashchenko999 commented 1 year ago

Dear Developer,

I trust this message reaches you in high spirits. I want to express my sincere appreciation for the react-hot-toast library. This tool has proven integral to my projects, offering a highly intuitive user interface that is both practical and efficient.

Recently, I transitioned from using react-toastify to react-hot-toast. However, I noticed the absence of a feature that was available in react-toastify: the 'limit' parameter.

The 'limit' parameter in react-toastify ensures only a predefined number of toasts are displayed at any given moment, generating an efficient queue system. Consequently, when multiple toasts are triggered simultaneously, the screen does not become overcrowded, enhancing user experience.

Presently, the react-hot-toast library interrupts toast displays. This can compromise comprehension for the user, as toasts may disappear before they have had sufficient time to read them. Below is the current implementation:

  const { toasts } = useToasterStore()

  const TOAST_LIMIT = 5

  // Enforce Limit
  useEffect(() => {
    toasts
      .filter(t => t.visible) // Only consider visible toasts
      .filter((item, i) => i >= TOAST_LIMIT) // Is toast index over limit
      .forEach(t => toast.dismiss(t.id)) // Dismiss – Use toast.remove(t.id) removal without animation
  }, [toasts])

By incorporating a feature that regulates the number of displayed toasts, I believe the react-hot-toast library will be greatly enhanced. This added functionality will ensure that toasts can be read and processed effectively by the user, fostering an improved user experience.

I kindly ask that you consider this feature in your subsequent updates, as I believe it would greatly benefit all users of the react-hot-toast library.

Thank you for your continuous dedication and hard work on this exceptional library. Your efforts are genuinely appreciated.

Warm regards.

ADTC commented 1 year ago

This is nice. I've wanted to do this. How do I limit this to a certain class of toasts only (and not apply to other toasts)? For example we have regular toasts that show at top-center, and "notification" toasts that show at top-right corner. I want the limit to apply only on the "notification" toasts while the regular toasts should be unlimited, and shouldn't affect the "notification" toasts.

~I thought of a crude hack of using a random number generator with a fixed prefix as the id then in the above code, add one more filter to get only the toasts that have the prefix in the id.~

I'd love to see a cleaner solution though. Could we use className for this purpose? Is there a type property? Could we just add some custom property to the toasts which is retrievable in useToasterStore?

ADTC commented 1 year ago

Update: It looks like you can attach className, type or any custom property to the toast, simply by specifying it as an option. Then you can use it to filter. However, className could be used for actual CSS styles and I don't recommend using type. It seems this shows a green checkmark when type isn't blank or error.

In this example I defined a custom property toastType:

// example toast:
  toast( ... , { toastType: 'notification', ... });

// how to filter and remove excess:
    toasts
      .filter(t => t.toastType === 'notification') // Only consider notifications
      .filter(t => t.visible) // Only consider visible toasts
      // BONUS TIP: You can combine the above two to:
      // .filter(t => t.visible && t.toastType === 'notification')
      .filter((item, i) => i >= TOAST_LIMIT) // Is toast index over limit?
      .forEach(t => toast.dismiss(t.id)) // Dismiss – Use toast.remove to hide without animation
ivashchenko999 commented 1 year ago

Thanks, it's not a bad idea to use classes, I hope that soon we will see the ability to set a queue in the API

ADTC commented 1 year ago

@ivashchenko999 I ended up changing that to toastType though because I used className for some real CSS styling. I should update my answer.

conc2304 commented 1 year ago

So I feel like the solutions above hide the problem that you are just discarding any message that comes in past the TOAST_LIMIT so you could be hiding otherwise useful information. Additionally filtering what you render to just using visible means that you lose the ability to trigger animations for entering and exiting that you can otherwise do by keying off of the visible prop. So after beating my head against a table on this for a while, I ended up creating a hook that can be used for message queueing.

import { useEffect, useState } from 'react';
import toast, { ToastOptions, ValueOrFunction, Renderable, Toast, useToasterStore } from 'react-hot-toast';

type ToastArgs = { message: ValueOrFunction<Renderable, Toast>; options: ToastOptions };
export const useToastQueue = () => {
  const MAX_CONCURRENT_TOASTS = 4;
  const DURATION = 4000;
  const [queue, setQueue] = useState<ToastArgs[]>([]);
  const { toasts } = useToasterStore();

  const addToastToQueue = (message: ValueOrFunction<Renderable, Toast>, options: ToastOptions = {}) => {
    setQueue((prevQueue) => [...prevQueue, { message, options }]);
  };

  useEffect(() => {
    const availableSlots = MAX_CONCURRENT_TOASTS - toasts.length;

    if (availableSlots > 0 && queue.length > 0) {
      const toastsToShow = queue.slice(0, availableSlots);
      const remainingToasts = queue.slice(availableSlots);

      setTimeout(() => {
        toastsToShow.forEach((toastItem) => {
          toast(toastItem.message, { duration: DURATION, ...toastItem.options });
        });
        setQueue(remainingToasts);
      }, 1); // break the race condition of useEffect
    }
  }, [toasts, queue]);

  return { addToastToQueue };
};

and then implemented in a component as such:

...
 const { addToastToQueue } = useToastQueue();
addToastToQueue(`BANANA`, toastOptions);
...
ADTC commented 1 year ago

@conc2304 neat hook. I suppose it is more useful when you want all notifications to have equal airtime.

Btw, you can just do return addToastToQueue; then you can name it anything you want when you use the hook:

const toast = useToastQueue();
toast(`BANANA`, toastOptions);

or:

const grill = useToastQueue();
grill(`BURGER`, toastOptions);