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