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