use-toast.ts 3.9 KB

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