跳到主要内容

使用useContext封装一个全局useToast

· 阅读需 3 分钟
Slipeda
ToastContext.ts
import { createContext } from 'react'

interface ToastContextProps {
toast: {
success: (message: string, duration?: number) => void
error: (message: string, duration?: number) => void
}
}

export const ToastContext = createContext<ToastContextProps | undefined>(
undefined
)
emitter.ts
import mitt from 'mitt'

export const emitter = mitt()
GlobalToastProvider.tsx
import { Toast } from '@shopify/polaris'
import { useEffect, useMemo, useState } from 'react'
import { uniqueCode } from '../../utils'
import { ToastContext } from './ToastContext'
import { emitter } from './emitter'

/**
* Toasts 传参
*/
type ToastsProps = {
/** 前端生成的唯一id */
id: string
/** toast 内容 */
message: string
/** toast 类型 */
type: 'success' | 'error'
/** toast 持续时间 */
duration: number
/** 插入队列的时间 */
createdAt: Date
}

export const GlobalToastProvider = ({
children,
}: {
children: React.ReactNode
}) => {
/** toasts 队列 */
const [toasts, setToasts] = useState<ToastsProps[]>([])

/** 唤起成功的 toast */
const toastSuccess = (message: string, duration = 5000) => {
const newToast: ToastsProps = {
id: uniqueCode('toast_success'),
type: 'success',
message,
duration,
createdAt: new Date(), // 记录插入队列的时间
}

setToasts((prevToasts) => [...prevToasts, newToast])
}

/** 唤起错误的 toast */
const toastError = (message: string, duration = 5000) => {
const newToast: ToastsProps = {
id: uniqueCode('toast_error'),
type: 'error',
message,
duration,
createdAt: new Date(), // 记录插入队列的时间
}

setToasts((prevToasts) => [...prevToasts, newToast])
}

/** toast 唤起函数 */
const toastHandler = useMemo(() => {
return {
success: toastSuccess,
error: toastError,
}
}, [])

/** 移除 toast */
const dismissToast = (id: string) => {
setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id))
}

/** 定时器根据入队时间和 duration 比较确定 toast 存在时长 */
useEffect(() => {
let timer: NodeJS.Timeout
if (toasts.length === 0) return

timer = setInterval(() => {
const now = new Date()
const updatedToasts = toasts.filter((toast) => {
return now.getTime() - toast.createdAt.getTime() < toast.duration
})
setToasts(updatedToasts)
}, 1000)

return () => {
clearInterval(timer)
}
}, [toasts])

useEffect(() => {
emitter.on('toaster.error', (e: any) =>
toastHandler.error(e.message, e.duration)
)
emitter.on('toaster.success', (e: any) =>
toastHandler.success(e.message, e.duration)
)
return () => {
emitter.all.clear()
}
}, [toastHandler])

return (
<ToastContext.Provider value={{ toast: toastHandler }}>
{children}
{toasts.map((item) => (
<Toast
key={item.id}
content={item.message}
onDismiss={() => dismissToast(item.id)}
error={item.type === 'error'}
/>
))}
</ToastContext.Provider>
)
}
useToast
import { useContext } from 'react'
import { ToastContext } from './ToastContext'

type useToastValue = {
success: (message: string, duration?: number) => void
error: (message: string, duration?: number) => void
}

export const useToast = (): useToastValue => {
const context = useContext(ToastContext)

if (!context) {
throw new Error(
'useToast must be used within a <GlobalToastProvider> * </GlobalToastProvider>'
)
}

return context.toast
}
toaster.ts
import { emitter } from './emitter'

export const toaster = {
success: (message: string, duration?: number) => {
emitter.emit('toaster.success', { message, duration })
},
error: (message: string, duration?: number) => {
emitter.emit('toaster.error', { message, duration })
},
}