使用react的createPortal实现全局消息提示组件

使用react的createPortal实现全局消息提示组件

DansRoh Lv4

如何封装一个全局的消息提示组件

心路历程:

在我的mini-bolg开发过程中,需要封装一个消息提示组件,项目基于nextjs+daisyui(一个基于tailwinds的ui库)开发。在组件封装的过程中,遇到了一些问题,在此记录一下==

项目环境:

1
2
3
4
5
6
7
"next": "14.2.2",
"react": "^18",
"react-dom": "^18",
"daisyui": "^4.10.2",
"uuid": "^9.0.1",
"uuidv4": "^6.2.13"
"tailwindcss": "^3.4.1",

组件需求描述

  1. 此组件用于操作后的提示信息,如请求失败后,页面提示错误信息。

    示例

  2. 用法类似于antd组件库的message,如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import {useToast} from "@/app/lib/components/Toast";
    const Page() => {
    const [toastApi, container] = useToast()
    // 调用info类型的提示信息
    toast.info(message)

    return (
    <div>
    {container}
    // 其他内容
    </div>
    )
    }
  3. 组件DOM应该渲染在body节点下

组件实现

  1. 定义类型文件/components/Toast/types.tsx

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    export type ToastStatus = "success" | "error" | "warning" | "info";

    export interface INotice {
    status: ToastStatus;
    text: string;
    key: string;
    }

    export interface ToastProps {
    key: string;
    text: string;
    status: ToastStatus;
    }

    export interface ToastApi {
    info: (text: string, delay?: number) => void;
    success: (text: string, delay?: number) => void;
    warning: (text: string, delay?: number) => void;
    error: (text: string, delay?: number) => void;
    }

  2. 首先在components/Toast/目录下定义一个Toast.tsx文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import {FC} from "react";
    import {ToastProps} from "@/lib/components/Toast/types";
    import classNames from "classnames";

    const Toast:FC<ToastProps> = (props) => {
    const { text, status } = props

    const classnames = classNames("alert","animate__fadeInDown", {
    "alert-info": status === "info",
    "alert-success": status === "success",
    "alert-error": status === "error",
    "alert-warn": status === "warning",
    })
    return (
    <div className={classnames}>
    <span>{text}</span>
    </div>
    )
    }

    export default Toast;
  3. 定义useToast.tsx文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    import {ReactPortal, useState} from "react";
    import {INotice, ToastApi, ToastStatus} from "@/lib/components/Toast/types";
    import {v4} from "uuid";
    import {createPortal} from "react-dom";
    import Toast from "@/lib/components/Toast/Toast";

    const useToast: ()=>[toastApi: ToastApi, container: ReactPortal | false] = () => {
    const [notices, setNotices] = useState<INotice[]>([])

    const container= notices.length>0 && createPortal(<div className='toast toast-top toast-center'>
    {
    notices.length > 0 && notices.map((notice:INotice) => (
    <Toast key={notice.key} text={notice.text} status={notice.status}></Toast>
    ))
    }
    </div>, document.body)

    const add = (notice: INotice) => {
    setNotices((prevState) => [...prevState, notice])
    }
    const remove = (key:string) => {
    setNotices(prevState => prevState.filter(v=>v.key !== key))
    }

    const createNotice = (text: string, status: ToastStatus, key: string, delay: number) => {
    add({
    text,
    status,
    key
    })
    setTimeout(() => remove(key), delay)
    }

    const toastApi = {
    warning: (text: string, delay:number=3000) => {
    createNotice(text, 'warning', v4(), delay)
    },
    success: (text: string, delay:number=3000) => {
    createNotice(text, 'success', v4(), delay)
    },
    info: (text: string, delay:number=3000) => {
    createNotice(text, 'info', v4(), delay)
    },
    error: (text: string, delay:number=3000) => {
    createNotice(text, 'error', v4(), delay)
    }
    }

    return [toastApi, container]
    }
    export default useToast
  4. 最后定义index.tsx文件导出需要的模块

    1
    export { default as useToast } from './useToast'

组件封装的核心

  • 使用createPortal()方法,像body添加Toast组件
1
2
3
4
5
6
7
const container= notices.length>0 && createPortal(<div className='toast toast-top toast-center'>
{
notices.length > 0 && notices.map((notice:INotice) => (
<Toast key={notice.key} text={notice.text} status={notice.status}></Toast>
))
}
</div>, document.body)

遇到的问题

  • 起初对createPortal的用法理解错了,以为和createRoot,然后掉用render方法是一样的。但其实,render方法是向节点插入一个元素,而createPortal是创造一个react的组件,把这个组件放在react的jsx代码中, 尽管这个组件看起来可能在你的id为app的div之下,但是在渲染的时候react会将他插入,你指定的位置,也就是createPortal的第二个参数。妙啊~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 返回的一个组件
const container= notices.length>0 && createPortal(<div className='toast toast-top toast-center'>
{
notices.length > 0 && notices.map((notice:INotice) => (
<Toast key={notice.key} text={notice.text} status={notice.status}></Toast>
))
}
</div>, document.body)

// 放在id为app的div中,实际渲染在document.body下
<div id={'app'} className="hero min-h-screen bg-base-200">
{container}
<div className="hero-content flex-col lg:flex-row-reverse">
<div className="text-center lg:text-left">
.....省略
  • 标题: 使用react的createPortal实现全局消息提示组件
  • 作者: DansRoh
  • 创建于 : 2024-04-29 00:00:00
  • 更新于 : 2024-06-24 16:20:56
  • 链接: https://blog.shinri.me/2024/04/29/23_封装一个react全局消息提示/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论