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

906 阅读5分钟

eae3a5e2359c5df7a833ad0d17f15481.jpg

前言

本文主要介绍了如何在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 下,代码如下:

// 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:useRefuseImperativeHandle

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-groupCSSTransition组件。

我们需要在项目中引入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提示的技术文章的分享,希望对大家有所帮助。如果有任何问题或建议,欢迎留言讨论!更多请关注公众号:【前端一叶子】

历史文章

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

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

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