Toast Messages — React Training

In this video we will learn how to create a “Toast” message system in React.

00:00 Introduction
01:35 1st approach, creation of the context
06:30 Disadvantage (re-rendered)
06:50 2nd approach, with a ref
11:40 Added duration
13:20 Added animation
17:57 Timer optimization

Purpose

Our goal is to expose a method that makes it easy to send a new Toast message from any component in our application. So we start to think about the API that we think is the most suitable and we build from that.

const {pushToast} = useToasts();

const onSubmit = () => {
    pushToast({title: 'Votre message a bien été sauvegardé', type: 'success', duration: 5})
}

The method pushToast() will take as parameters the properties to send to our component Toast. You can also specify a duration if you wish.

Organization

The first approach would be to create a context which contains the messages to be displayed and which would be updated each time a new message is sent. The disadvantage of this approach is that the context will be re-rendered with each new message. The consumers of the context will then also be re-rendered because of the change in value. Since the toast system is global a better solution is to use a ref system.

const defaultPush = (toast) => {}; // Méthode de base que l'on mettra dans le contexte par défaut

const ToastContext = createContext({
  pushToastRef: { current: defaultPush },
});

// On entourera notre application de ce provider pour rendre le toasts fonctionnel
export function ToastContextProvider({ children }: PropsWithChildren) {
  const pushToastRef = useRef(defaultPush);
  return (
    <ToastContext.Provider value={{ pushToastRef }}>
      <Toasts />
      {children}
    </ToastContext.Provider>
  );
}

The component Toasts will be able to retrieve the context and modify the value in the ref to add the desired behavior. If you want to add a appearing/disappearing animation it is possible to use framer-motion but you can use lighter libraries (like AutoAnimate) if you want.

import { AnimatePresence, motion } from "framer-motion";

export function Toasts() {
  const [toasts, setToasts] = useState([]);
  // On modifie la méthode du contexte
  const { pushToastRef } = useContext(ToastContext);
  pushToastRef.current = ({ duration, ...props }) => {
    // On génère un id pour différencier les messages
    const id = Date.now();
    // On sauvegarde le timer pour pouvoir l'annuler si le message est fermé
    const timer = setTimeout(() => {
      setToasts((v) => v.filter(
    }, (duration ?? 5) * 1000);
    const toast = { ...props, id, timer };
    setToasts((v) => [...v, toast]);
  };

  const onRemove = (toast: ToastItem) => {
    clearTimeout(toast.timer);
    setToasts((v) => v.filter(
  };

  return (
    <div className="toast-container">
      <AnimatePresence>
        {toasts.map((toast) => (
          <motion.div
            onClick={() => onRemove(toast)}
            key={toast.id}
            initial={{ opacity: 0, x: -30 }}
            animate={{ opacity: 1, x: 0 }}
            exit={{ opacity: 0, x: 30 }}
          >
            <Toast {...toast} />
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  );
}

Now all we have to do is create the hook that will provide the method pushToast() to be able to send a message from any component of our application.

export function useToasts() {
  const { pushToastRef } = useContext(ToastContext);
  return {
    pushToast: useCallback(
      (toast) => {
        pushToastRef.current(toast);
      },
      [pushToastRef]
    ),
  };
}

We will use a useCallback() to make sure to have only one version of this function to avoid rendering if this method is passed directly to a pure component.