crumpled paper texture

React Hot Toast

Current favorite lib for toast notifications, with some custom hook and implementation

––– views

4 min read

0 likes

React Hot Toast

A lib for toast notifications, my current favorite. Checkout the documentation

Package Dependencies

yarn add react-hot-toast

Set up

Add the div to _app.ts or App.tsx

import { AppProps } from 'next/app';
 
import '@/styles/globals.css';
 
import DismissableToast from '@/components/DismissableToast';
 
function MyApp({ Component, pageProps }: AppProps) {
  return (
    <>
      <DismissableToast />
      <Component {...pageProps} />
    </>
  );
}
 
export default MyApp;

DismissableToast

React hot toast doesn't support dismiss button by default, so I use a custom component to add it.

import * as React from 'react';
import { toast, ToastBar, Toaster } from 'react-hot-toast';
import { HiX } from 'react-icons/hi';
 
export default function DismissableToast() {
  return (
    <div>
      <Toaster
        reverseOrder={false}
        position='top-center'
        toastOptions={{
          style: {
            borderRadius: '8px',
            background: '#333',
            color: '#fff',
          },
        }}
      >
        {(t) => (
          <ToastBar toast={t}>
            {({ icon, message }) => (
              <>
                {icon}
                {message}
                {t.type !== 'loading' && (
                  <button
                    className='rounded-full p-1 ring-primary-400 transition hover:bg-[#444] focus:outline-none focus-visible:ring'
                    onClick={() => toast.dismiss(t.id)}
                  >
                    <HiX />
                  </button>
                )}
              </>
            )}
          </ToastBar>
        )}
      </Toaster>
    </div>
  );
}

Usage

import toast from 'react-hot-toast';
 
function addSomething() {
  toast.success('message');
  toast.error('message');
}

Toast Promise Example

With promise, we don't need to use toast.success or toast.error, but directly send a promise, and it will be managed.

toast.promise(
  axios
    .post('/user/login', data)
    .then((res) => {
      const { jwt: token } = res.data.data;
      tempToken = token;
      localStorage.setItem('token', token);
 
      // chaining axios in 1 promise
      return axios.get('/user/get-user-info');
    })
    .then((user) => {
      const role = user.data.data.user_role;
      dispatch('LOGIN', { ...user.data.data, token: tempToken });
 
      history.replace('/');
    }),
  {
    loading: 'Loading...',
    success: 'Success',
    error: (err) => err.response.data.msg,
  }
);

Default Message

You can compose it with default message

export const defaultToastMessage = {
  loading: 'Loading...',
  success: 'Data fetched successfully',
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  error: (err: any) =>
    err?.response?.data?.msg ?? 'Something is wrong, please try again',
};

useLoadingToast hook

Using a useLoadingToast hook we can get access if there is any loading toast showing, then use it to disable button.

import { useToasterStore } from 'react-hot-toast';
 
/**
 * Hook to get information whether something is loading
 * @returns true if there is a loading toast
 * @example const isLoading = useLoadingToast();
 */
export default function useLoadingToast(): boolean {
  const { toasts } = useToasterStore();
  const isLoading = toasts.some((toast) => toast.type === 'loading');
  return isLoading;
}

useWithToast hook

This hook will handle loading and error state from SWR and show toast on initial fetch. Revalidating will not trigger loading toast.

import * as React from 'react';
import toast from 'react-hot-toast';
import { SWRResponse } from 'swr';
 
import { defaultToastMessage } from '@/lib/helper';
 
import useLoadingToast from '@/hooks/useLoadingToast';
 
type OptionType = {
  runCondition?: boolean;
  loading?: string;
  success?: string;
  error?: string;
};
 
export default function useWithToast<T, E>(
  swr: SWRResponse<T, E>,
  { runCondition = true, ...customMessages }: OptionType = {}
) {
  const { data, error } = swr;
 
  const toastStatus = React.useRef<string>(data ? 'done' : 'idle');
 
  const toastMessage = {
    ...defaultToastMessage,
    ...customMessages,
  };
 
  React.useEffect(() => {
    if (!runCondition) return;
 
    // if toastStatus is done,
    // then it is not the first render or the data is already cached
    if (toastStatus.current === 'done') return;
 
    if (error) {
      toast.error(toastMessage.error, { id: toastStatus.current });
      toastStatus.current = 'done';
    } else if (data) {
      toast.success(toastMessage.success, { id: toastStatus.current });
      toastStatus.current = 'done';
    } else {
      toastStatus.current = toast.loading(toastMessage.loading);
    }
 
    return () => {
      toast.dismiss(toastStatus.current);
    };
  }, [
    data,
    error,
    runCondition,
    toastMessage.error,
    toastMessage.loading,
    toastMessage.success,
  ]);
 
  return { ...swr, isLoading: useLoadingToast() };
}

Usage

You can use it with the useSWR hook

const { data: pokemonData, isLoading } = useWithToast(
  useSWR<PokemonList>('https://pokeapi.co/api/v2/pokemon?limit=20'),
  {
    loading: 'Override Loading',
  }
);