Messages toasts avec React

Résumé Support

Dans cette vidéo nous allons apprendre comment créer un système de messages "Toast" dans React.

00:00 Introduction
01:35 1ère approche, création du contexte
06:30 Inconvénient (rerendu)
06:50 2ème approche, avec une ref
11:40 Ajout de la durée
13:20 Ajout de l'animation
17:57 Optimisation du timer

Objectif

Notre objectif est d'exposer une méthode qui permette d'envoyer facilement un nouveau message Toast depuis n'importe quel composant dans notre application. On commence donc à réfléchir à l'API qui nous semble la plus adapté et on construit à partir de ça.

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

La méthode pushToast() prendra en paramètre les propriétés à envoyer à notre composant Toast. On pourra aussi spécifier une durée si on le souhaite.

Organisation

La première approche serait de créer un contexte qui contient les messages à afficher et qui serait mis à jour à chaque fois que l'on envoie un nouveau message. L'inconvénient de cette approche est que le contexte sera re-rendu à chaque nouveau message. Les consommateurs du contexte seront alors eux-aussi re-rendu à cause du changement de valeur. Vu que le système de toast est global une meilleur solution est d'utiliser un système de ref.

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> ); }

Le composant Toasts pourra récupérer le contexte et venir modifier la valeur dans la ref pour ajouter le comportement souhaité. Si vous voulez ajouter une animation d'apparition / disparition il est possible d'utiliser framer-motion mais vous pouvez utiliser des librairies plus légère (comme AutoAnimate) si vous le souhaitez.

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((t) => t.id !== id)); }, (duration ?? 5) * 1000); const toast = { ...props, id, timer }; setToasts((v) => [...v, toast]); }; const onRemove = (toast: ToastItem) => { clearTimeout(toast.timer); setToasts((v) => v.filter((t) => t !== toast)); }; 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> ); }

Il ne nous reste plus qu'à créer le hook qui fournira la méthode pushToast() pour pouvoir envoyer un message depuis n'importe quel composant de notre application.

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

On utilisera un useCallback() afin de s'assurer de n'avoir qu'une seule version de cette fonction pour éviter les rendu si cette méthode est passée directement à un composant pure.