use-toast.ts 3.8 KB

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