/ components / ui / use-toast.js
use-toast.js
  1  "use client";;
  2  // Inspired by react-hot-toast library
  3  import * as React from "react"
  4  
  5  const TOAST_LIMIT = 1
  6  const TOAST_REMOVE_DELAY = 1000000
  7  
  8  const actionTypes = {
  9    ADD_TOAST: "ADD_TOAST",
 10    UPDATE_TOAST: "UPDATE_TOAST",
 11    DISMISS_TOAST: "DISMISS_TOAST",
 12    REMOVE_TOAST: "REMOVE_TOAST"
 13  }
 14  
 15  let count = 0
 16  
 17  function genId() {
 18    count = (count + 1) % Number.MAX_SAFE_INTEGER
 19    return count.toString();
 20  }
 21  
 22  const toastTimeouts = new Map()
 23  
 24  const addToRemoveQueue = (toastId) => {
 25    if (toastTimeouts.has(toastId)) {
 26      return
 27    }
 28  
 29    const timeout = setTimeout(() => {
 30      toastTimeouts.delete(toastId)
 31      dispatch({
 32        type: "REMOVE_TOAST",
 33        toastId: toastId,
 34      })
 35    }, TOAST_REMOVE_DELAY)
 36  
 37    toastTimeouts.set(toastId, timeout)
 38  }
 39  
 40  export const reducer = (state, action) => {
 41    switch (action.type) {
 42      case "ADD_TOAST":
 43        return {
 44          ...state,
 45          toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
 46        };
 47  
 48      case "UPDATE_TOAST":
 49        return {
 50          ...state,
 51          toasts: state.toasts.map((t) =>
 52            t.id === action.toast.id ? { ...t, ...action.toast } : t),
 53        };
 54  
 55      case "DISMISS_TOAST": {
 56        const { toastId } = action
 57  
 58        // ! Side effects ! - This could be extracted into a dismissToast() action,
 59        // but I'll keep it here for simplicity
 60        if (toastId) {
 61          addToRemoveQueue(toastId)
 62        } else {
 63          state.toasts.forEach((toast) => {
 64            addToRemoveQueue(toast.id)
 65          })
 66        }
 67  
 68        return {
 69          ...state,
 70          toasts: state.toasts.map((t) =>
 71            t.id === toastId || toastId === undefined
 72              ? {
 73                  ...t,
 74                  open: false,
 75                }
 76              : t),
 77        };
 78      }
 79      case "REMOVE_TOAST":
 80        if (action.toastId === undefined) {
 81          return {
 82            ...state,
 83            toasts: [],
 84          }
 85        }
 86        return {
 87          ...state,
 88          toasts: state.toasts.filter((t) => t.id !== action.toastId),
 89        };
 90    }
 91  }
 92  
 93  const listeners = []
 94  
 95  let memoryState = { toasts: [] }
 96  
 97  function dispatch(action) {
 98    memoryState = reducer(memoryState, action)
 99    listeners.forEach((listener) => {
100      listener(memoryState)
101    })
102  }
103  
104  function toast({
105    ...props
106  }) {
107    const id = genId()
108  
109    const update = (props) =>
110      dispatch({
111        type: "UPDATE_TOAST",
112        toast: { ...props, id },
113      })
114    const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
115  
116    dispatch({
117      type: "ADD_TOAST",
118      toast: {
119        ...props,
120        id,
121        open: true,
122        onOpenChange: (open) => {
123          if (!open) dismiss()
124        },
125      },
126    })
127  
128    return {
129      id: id,
130      dismiss,
131      update,
132    }
133  }
134  
135  function useToast() {
136    const [state, setState] = React.useState(memoryState)
137  
138    React.useEffect(() => {
139      listeners.push(setState)
140      return () => {
141        const index = listeners.indexOf(setState)
142        if (index > -1) {
143          listeners.splice(index, 1)
144        }
145      };
146    }, [state])
147  
148    return {
149      ...state,
150      toast,
151      dismiss: (toastId) => dispatch({ type: "DISMISS_TOAST", toastId }),
152    };
153  }
154  
155  export { useToast, toast }