useToast.ts 4.0 KB

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