use-toast.ts 3.8 KB

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