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.