前言
本文主要介绍了如何在React中模拟antd封装一个简单的 Toast提示组件。在实际的项目开发中,对于使用Toast提示(类似antd中 Message/Notification 提示)的场景比较多,当我们对组件定制化要求比较高的时候,可以考虑自定义一个Toast组件。
Demo展示:
Toast组件实现
本文的demo使用的框架是
react + vite + tailwindcss
,自动生成脚手架参考:vite官网
Toast容器
首先需要我们需要一个总容器来放置 Toast消息,作为一个通用组件,为了保证Toast与页面的内容独立,因此我门需要使用 React 16的一个Api,ReactDom.createPortal
, 将Toast组件渲染到 body
下,代码如下:
// Toast/index.tsx
import { createPortal } from 'react-dom'
const ToastContainer = () => {
const renderDom = (
<div className='fixed top-0 left-0 right-0 z-[999] flex justify-center flex-col pt-[20px]'>
</div>
)
return typeof document !== 'undefined' ? createPortal(renderDom, document.body) : renderDom
}
export default ToastContainer
然后将Toast
引入到 APP.tsx 中
// APP.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Home from './pages/home'
import ToastContainer from '@/components/ToastContainer'
import './App.css'
const App = () => {
return (
<BrowserRouter>
<Routes>
<Route path='/' element={<Home />} />
</Routes>
<ToastContainer />
</BrowserRouter>
)
}
export default App
我们可以在控制台中看到Toast容器已经渲染在最外层了。
Toast消息内容
容器创建好后,我们需要画一下消息内容的样式。
新建一个 ToastMessage.tsx 文件,代码如下:
// ToastMessage.tsx
import clsx from 'clsx'
const ToastMessage = () => {
return <div className={clsx(
'mt-[20px] mx-auto px-[40px] min-h-[40px] py-[8px] bg-[#F8F4F1] text-[#989391] text-[30px] font-playfair leading-[40px] rounded-lg',
)}>Hello World!</div>
}
export default ToastMessage
这时,我们可以看到页面的显示:
Toast消息列表
考虑到我们可能需要同时显示多个Toast提示,我们需要使用一个数组列表 toastList
来存储多个message消息条,每个消息条包含 id
| msg
| duration
等属性,如果有其他属性可以自行添加。
接着遍历 toastList
渲染所有消息内容。
// index.tsx
import { useState } from 'react'
import { createPortal } from 'react-dom'
import ToastMessage from './ToastMessage'
const ToastContainer = () => {
const [toastList, setToastList] = useState<{ id: string; msg: string; duration?: number }[]>([
{ id: '1', msg: 'Apple', duration: 3000 },
{ id: '2', msg: 'Banana', duration: 3000 },
])
const renderDom = (
<div className='fixed top-0 left-0 right-0 z-[999] flex justify-center flex-col pt-[20px]'>
{
toastList.map((item) => {
return <ToastMessage key={item.id} {...item}>{ item.msg }</ToastMessage>
})
}
</div>
)
return typeof document !== 'undefined' ? createPortal(renderDom, document.body) : renderDom
}
export default ToastContainer
同时将 ToastMessage.tsx 中的内容改成从父组件中传入:
// ToastMessage.tsx
import clsx from 'clsx'
import { FC } from 'react'
interface ToastMsgProps {
children?: React.ReactNode
duration?: number
}
const ToastMessage: FC<ToastMsgProps> = (props) => {
const {
children,
duration = 3000
} = props
return <div className={clsx(
'mt-[20px] mx-auto px-[40px] min-h-[40px] py-[8px] bg-[#F8F4F1] text-[#989391] text-[30px] font-playfair leading-[40px] rounded-lg',
)}>{ children }</div>
}
export default ToastMessage
可以看到此时页面会展示两个toast message
外部暴露方法
由于我们需要像antd
一样,可以通过直接调用方法 message.info()
| message.warnning()
的形式来实现消息提示,因此,在Toast的内容样式都画好后,我们还需要暴露组件的调用方法给外部使用。
这里我们引入了react的两个Hook:useRef
和 useImperativeHandle
。
useImperativeHandle主要用于子组件自定义一个ref暴露给外部调用。
本文只定义一个info方法作为示例,如果有需要的可以根据自己需求添加其他方法。
具体代码实现如下:
// index.tsx
import { useState, useImperativeHandle, useRef, useEffect } from 'react'
import { createPortal } from 'react-dom'
import ToastMessage from './ToastMessage'
interface IToastRef {
info: (msg: string, options?: { duration?: number }) => void
}
// eslint-disable-next-line react-refresh/only-export-components
export const toast: { current: IToastRef | null } = { current: null }
const ToastContainer = () => {
const toastRef = useRef<IToastRef>(null)
const [toastList, setToastList] = useState<{ id: string; msg: string; duration?: number }[]>([])
useImperativeHandle(toastRef, () => {
return {
info: (msg: string, option) => {
const item = {
msg, duration: option?.duration, id: `${+new Date()}`
}
return setToastList(list => [...list, item])
}
}
})
useEffect(() => {
toast.current = toastRef.current
}, [])
const renderDom = (
<div className='fixed top-0 left-0 right-0 z-[999] flex justify-center flex-col pt-[20px]'>
{
toastList.map((item) => {
return <ToastMessage key={item.id} {...item}>{ item.msg }</ToastMessage>
})
}
</div>
)
return typeof document !== 'undefined' ? createPortal(renderDom, document.body) : renderDom
}
export default ToastContainer
提示:这里的
setToastList
需要使用函数方式获取上一个列表状态,来设置最新的toastList
, 如果这样写 "setToastList([...toastList, item])", 则旧的toastList
拿到的永远都是空数组。(详看useState)
相应的,ToastMessage.tsx 组件也需要进行调整:
// ToastMessage.tsx
import clsx from 'clsx'
import { FC, useState, useEffect } from 'react'
interface ToastMsgProps {
children?: React.ReactNode
duration?: number
}
const ToastMessage: FC<ToastMsgProps> = (props) => {
const {
children,
duration = 3000
} = props
const [visible, setVisible] = useState(false)
useEffect(() => {
setVisible(true)
setTimeout(() => {
setVisible(false)
}, duration)
}, [])
return visible && <div
className={clsx(
'mt-[20px] mx-auto px-[40px] min-h-[40px] py-[8px] bg-[#F8F4F1] text-[#989391] text-[30px] font-playfair leading-[40px] rounded-lg',
)}>{ children }</div>
}
export default ToastMessage
接着我们在页面中添加一个一个按钮,用来点击调用toast方法
// home.tsx
import { toast } from "@/components/ToastContainer";
const Home = () => {
const handleShowToast = () => {
toast.current?.info('Hello World!', {
duration: 2000
})
}
return (
<>
<button className='mt-[300px] rounded-xl bg-[#7ab7f8] text-[#fff] px-[12px] py-[8px] ' onClick={() => handleShowToast()}>
Show Toast
</button>
</>
)
}
export default Home
这时候点击Show Toast
按钮,就可以看到页面Toast提示,并且该提示会在duration
时间内销毁。
动画实现 CSSTransition
到这里,Toast组件的功能基本实现了。但是现在弹出提示的过程会显得比较生硬。接下来我们给Toast组件加下动画,让它看起来顺滑一些, 这里我们使用的是react-transition-group
的CSSTransition
组件。
我们需要在项目中引入react-transition-group
# pnpm
pnpm install react-transition-group @types/react-transition-group
# npm
npm i react-transition-group @types/react-transition-group
然后在ToastMessage中添加CSSTransition组件,ToastMessage.tsx 文件改造如下:
// ToastMessage.tsx
import clsx from 'clsx'
import { FC, useState, useEffect, useRef } from 'react'
import { CSSTransition } from 'react-transition-group'
import styles from './index.module.css'
interface ToastMsgProps {
children?: React.ReactNode
duration?: number
}
const ToastMessage: FC<ToastMsgProps> = (props) => {
const {
children,
duration = 3000
} = props
const msgRef = useRef(null)
const [visible, setVisible] = useState(false)
const [enter, setEnter] = useState(false)
useEffect(() => {
setVisible(true)
setTimeout(() => {
setEnter(true)
}, 100)
}, [])
return visible && <CSSTransition
nodeRef={msgRef}
in={enter}
unmountOnExit
timeout={duration}
classNames="my-toast-msg"
onEntered={() =>{
setEnter(false)
}}
onExiting={() => {
setTimeout(() => {
setVisible(false)
}, 300);
}}
>
<div
ref={msgRef}
className={clsx(
'mt-[20px] mx-auto px-[40px] min-h-[40px] py-[8px] bg-[#F8F4F1] text-[#989391] text-[30px] font-playfair leading-[40px] rounded-lg',
styles.my_toast_msg
)}>{ children }</div>
</CSSTransition>
}
export default ToastMessage
现在,我们的Toast组件就完成啦,现在,我们可以尝试再点击Show Toast按钮,查看最终的效果如下。
总结
本文主要介绍了如何使用React实现一个简单的Toast组件,通过封装自定义组件,我们可以在全局方便的调用Toast,我们能够更好地管理和定制Toast的样式和动画效果,并且能够加深对React的组件封装技术的理解。以上就是我对使用React封装Toast提示的技术文章的分享,希望对大家有所帮助。如果有任何问题或建议,欢迎留言讨论!更多请关注公众号:【前端一叶子】