mx / src /hooks /use-toast.ts
Ethscriptions's picture
Step 1: 项目计划与分镜大纲 README
363c51a verified
Raw History Blame Contribute Delete
3.19 kB
import * as React from "react"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: React.ReactElement
open?: boolean
onOpenChange?: (open: boolean) => void
variant?: "default" | "destructive"
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type Action =
| { type: typeof actionTypes.ADD_TOAST; toast: ToasterToast }
| { type: typeof actionTypes.UPDATE_TOAST; toast: Partial<ToasterToast> }
| { type: typeof actionTypes.DISMISS_TOAST; toastId?: string }
| { type: typeof actionTypes.REMOVE_TOAST; toastId?: string }
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) return
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({ type: actionTypes.REMOVE_TOAST, toastId })
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case actionTypes.ADD_TOAST:
return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT) }
case actionTypes.UPDATE_TOAST:
return { ...state, toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)) }
case actionTypes.DISMISS_TOAST: {
const { toastId } = action
if (toastId) addToRemoveQueue(toastId)
else state.toasts.forEach((t) => addToRemoveQueue(t.id))
return { ...state, toasts: state.toasts.map((t) => (toastId == null || t.id === toastId ? { ...t, open: false } : t)) }
}
case actionTypes.REMOVE_TOAST:
return { ...state, toasts: action.toastId == null ? [] : state.toasts.filter((t) => t.id !== action.toastId) }
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => listener(memoryState))
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) => dispatch({ type: actionTypes.UPDATE_TOAST, toast: { ...props, id } })
const dismiss = () => dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id })
dispatch({ type: actionTypes.ADD_TOAST, toast: { ...props, id, open: true, onOpenChange: (open) => { if (!open) dismiss() } } })
return { id, dismiss, update }
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => { const i = listeners.indexOf(setState); if (i > -1) listeners.splice(i, 1) }
}, [state])
return { ...state, toast, dismiss: (toastId?: string) => dispatch({ type: actionTypes.DISMISS_TOAST, toastId }) }
}
export { useToast, toast }