React Toast Component and Custom Hook using Tailwindcss
Toasts are basically just notifications. When you have a complex application or you need a lot of input from your users, giving them feedback on their actions is really important to their experience. Toasts are a great way to do this and here is a simple integration into a React application.
Table of contents
Toast Component
As you can see, this component is taking in a type and a message and it's configured to take in two types right now. Success and Error.
import { useToastDispatchContext } from "../context/ToastContext";
export default function Toast({ type, message, id }) {
const dispatch = useToastDispatchContext();
return (
<>
{type == "success" && (
<div className="rounded-md bg-green-50 p-4 m-3">
<div className="flex">
<div className="flex-shrink-0">
<svg
className="h-5 w-5 text-green-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="ml-3">
<p className="text-sm font-medium text-green-800">{message}</p>
</div>
<div className="ml-auto pl-3">
<div className="-mx-1.5 -my-1.5">
<button
onClick={() => {
dispatch({ type: "DELETE_TOAST", id });
}}
className="inline-flex bg-green-50 rounded-md p-1.5 text-green-500 hover:bg-green-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-green-50 focus:ring-green-600"
>
<span className="sr-only">Dismiss</span>
<svg
className="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
</div>
</div>
</div>
)}
{type == "error" && (
<div className="rounded-md bg-red-50 p-4 m-3">
<div className="flex">
<div className="flex-shrink-0">
<svg
class="h-5 w-5 text-red-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="ml-3">
<p className="text-sm font-medium text-red-800">{message}</p>
</div>
<div className="ml-auto pl-3">
<div className="-mx-1.5 -my-1.5">
<button
onClick={() => {
dispatch({ type: "DELETE_TOAST", id });
}}
className="inline-flex bg-red-50 rounded-md p-1.5 text-red-500 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-green-50 focus:ring-red-600"
>
<span className="sr-only">Dismiss</span>
<svg
className="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
</div>
</div>
</div>
)}
</>
);
}
Toast Container
The toast container is positioned absolute and will live at the top of our application.
import Toast from "./Toast";
import { useToastStateContext } from "../context/ToastContext";
export default function ToastContainer() {
const { toasts } = useToastStateContext();
return (
<div className="absolute bottom-10 w-full z-50">
<div className="max-w-xl mx-auto">
{toasts &&
toasts.map((toast) => (
<Toast
id={toast.id}
key={toast.id}
type={toast.type}
message={toast.message}
/>
))}
</div>
</div>
);
}
Bringing it together
We're now going to bring all this logic together so that we can use the Toast notifications within our application. This is a Next.js application so we're adding our logic to the _app.js file.
import "tailwindcss/tailwind.css";
import ToastContainer from "../components/ToastContainer";
import { ToastProvider } from "../context/ToastContext";
function MyApp({ Component, pageProps }) {
return (
<>
<ToastProvider>
<Component {...pageProps} />
<ToastContainer />
</ToastProvider>
</>
);
}
export default MyApp;
The custom hook.
The custom hook will be how we send off notifications. You can see we export a single function from the hook,
which is toast()
. We can now import this anywhere within our application and send off toast notifications.
import { useToastDispatchContext } from "../context/ToastContext";
export function useToast(delay) {
const dispatch = useToastDispatchContext();
function toast(type, message) {
const id = Math.random().toString(36).substr(2, 9);
dispatch({
type: "ADD_TOAST",
toast: {
type,
message,
id,
},
});
setTimeout(() => {
dispatch({ type: "DELETE_TOAST", id });
}, delay);
}
return toast;
}
Using the toast
This example is using the toast with a form submission. If you noticed were passing a delay to the useToast hook, which is 4 seconds here. This will mean the Toast is deleted after 4 seconds.
import { useToast } from '../hooks/useToast';
export default function Home() {
const { register, handleSubmit, errors, reset } = useForm();
const toast = useToast(4000);
const router = useRouter();
async function onSubmitForm(values) {
try {
const response = await axios(config);
console.log(response);
if (response.status == 200) {
toast('success', 'You have successfully submitted the form');
}
} catch (err) {
toast('error', 'Something went wrong, there was an error.');
}
}