Toasts
Toasts can be used to show contextual messages to the user that are not part of the page. A good example is showing an error message if a request to the backend fails. Another would be showing a success message after the user changes their password - as there is no direct confirmation via the page content.
Firstly lets create a ToastContext that we can use throughout the app
to add toasts as required by adding the following to
frontend/src/ToastContext.tsx
,
import { Color } from "@material-ui/lab/Alert";
import React from "react";
export interface IToast {
category?: Color;
message: string;
}
interface IToastContext {
addToast: (toast: IToast) => void;
setToasts: React.Dispatch<React.SetStateAction<IToast[]>>;
toasts: IToast[];
}
export const ToastContext = React.createContext<IToastContext>({
addToast: () => {},
setToasts: () => {},
toasts: [],
});
interface IProps {
children?: React.ReactNode;
}
export const ToastContextProvider = ({ children }: IProps) => {
const [toasts, setToasts] = React.useState<IToast[]>([]);
const addToast = (toast: IToast) => {
setToasts((toasts) => [...toasts, toast]);
};
return (
<ToastContext.Provider value={{ addToast, setToasts, toasts }}>
{children}
</ToastContext.Provider>
);
};
We'll use the Material-UI snackbar to display toasts, and the Material-UI Alert to style the snack depending on the category (error, success, etc). The Alert is currently part of the Material-UI lab that can be installed via npm,
Run this command in frontend/
npm install --save @material-ui/lab
We can then create a Toasts component to display them. Note that only
one toast should be displayed at any point in time, so a useEffect is
setup to take and display a toast whenever there are toasts to display
and there isn't an open one. The following should be added to
frontend/src/components/Toasts.tsx
,
import Snackbar from "@material-ui/core/Snackbar";
import Alert from "@material-ui/lab/Alert";
import React, { useEffect } from "react";
import { ToastContext, IToast } from "src/ToastContext";
const Toasts = () => {
const { toasts, setToasts } = React.useContext(ToastContext);
const [open, setOpen] = React.useState(false);
const [currentToast, setCurrentToast] = React.useState<IToast | undefined>();
useEffect(() => {
if (!open && toasts.length) {
setCurrentToast(toasts[0]);
setToasts((prev) => prev.slice(1));
setOpen(true);
}
}, [open, setCurrentToast, setOpen, setToasts, toasts]);
const onClose = (event?: React.SyntheticEvent, reason?: string) => {
if (reason !== "clickaway") {
setOpen(false);
}
};
return (
<Snackbar
anchorOrigin={{
horizontal: "center",
vertical: "top",
}}
autoHideDuration={6000}
onClose={onClose}
onExited={() => setCurrentToast(undefined)}
open={open}
>
<Alert
onClose={onClose}
severity={currentToast?.category}
variant="filled"
>
{currentToast?.message}
</Alert>
</Snackbar>
);
};
export default Toasts;
Finally we can add the following to src/App.tsx
to enable toasts in
the app,
...
import Toasts from "src/components/Toasts";
import { ToastContextProvider } from "src/ToastContext";
const App = () => {
...
return (
<ToastContextProvider>
<Toasts />
...
</ToastContextProvider>
);
}