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.