React封装自定义Toast组件(模仿antd)

前言

本文主要介绍了如何在React中模拟antd封装一个简单的 Toast提示组件。在实际的项目开发中,对于使用Toast提示(类似antd中 Message/Notification 提示)的场景比较多,当我们对组件定制化要求比较高的时候,可以考虑自定义一个Toast组件。

Demo展示:

github仓库地址

Toast组件实现

本文的demo使用的框架是react + vite + tailwindcss,自动生成脚手架参考:vite官网

Toast容器

首先需要我们需要一个总容器来放置 Toast消息,作为一个通用组件,为了保证Toast与页面的内容独立,因此我门需要使用 React 16的一个Api,ReactDom.createPortal, 将Toast组件渲染到 body 下,代码如下:

ts 复制代码
// 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

ts 复制代码
// 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 文件,代码如下:

ts 复制代码
// 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 渲染所有消息内容。

ts 复制代码
// 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 中的内容改成从父组件中传入:

ts 复制代码
// 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:useRefuseImperativeHandle

useImperativeHandle主要用于子组件自定义一个ref暴露给外部调用。

本文只定义一个info方法作为示例,如果有需要的可以根据自己需求添加其他方法。

具体代码实现如下:

ts 复制代码
// 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 组件也需要进行调整:

ts 复制代码
// 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方法

ts 复制代码
// 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-groupCSSTransition组件。

我们需要在项目中引入react-transition-group

bash 复制代码
# pnpm
pnpm install react-transition-group @types/react-transition-group

# npm
npm i react-transition-group @types/react-transition-group

然后在ToastMessage中添加CSSTransition组件,ToastMessage.tsx 文件改造如下:

ts 复制代码
// 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提示的技术文章的分享,希望对大家有所帮助。如果有任何问题或建议,欢迎留言讨论!更多请关注公众号:【前端一叶子】

历史文章

# 小白Websocket入门,10分钟搭建一个多人聊天室~

# 推荐10个实用的程序员开发常用工具

# TCP/IP协议族和TCP三次握手、四次挥手

相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax