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