use-toast.ts 3.8 KB

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