vlambda博客
学习文章列表

在Next.js中使用React Portals创建弹窗

React Portal 提供了一种将子节点渲染到父组件以外的 DOM 节点的优秀解决方案。

Portal 的最常见用例是子组件需要从视觉上脱离父容器:

  • 模态对话框
  • 工具提示
  • 悬浮卡
  • 加载动画

通常可以使用 ReactDOM.createPortal(child,container) 创建一个 Portal,例如创建一个简单地Modal:

const Modal = ({ message, isOpen, onClose, children }) => {
  if (!isOpen) return null;
  return ReactDOM.createPortal(
    <div className="modal">
      <span className="message">{message}</span>
      <button onClick={onClose}>Close</button>
    </div>
,
    document.body
  );
};

在Next.js中的使用方式不变。

现在我们创建一个传送门构造器,并使用其创建一个tip-modal。

首先在根组件中创建modal根节点,我们的tip-modal都挂载在这个modal根节点上

// _app.js
export default function MyApp({ Component, pageProps }{
  return (
    <>
      <div id="modal"></div>
      <Component {...pageProps} />
    </>

  )
}

创建传送门构造器 ClientOnlyPortal:

// components/ClientOnlyPortal.js
import { useRef, useEffect, useState } from 'react'
import { createPortal } from 'react-dom'

// 传送门构造器
export default function ClientOnlyPortal({ children, selector }{
  const ref = useRef()
  const [mounted, setMounted] = useState(false)

  useEffect(() => {
    // 获取传送门挂载点
    ref.current = document.querySelector(selector)
    setMounted(true)
  }, [selector])

  return mounted ? createPortal(children, ref.current) : null
}

// components/tip/tip.js
import styles from './tip.module.css'
import cn from 'classnames'
import ClientOnlyPortal from '../ClientOnlyPortal'

export default function Tip({ children, type }{
    return (
        <ClientOnlyPortal selector="#modal">
            <div className={cn({
                [styles.tip]: true,
                [styles.success_bg]: type === 'success',
                [styles.error_bg]: type === 'error'
            })}>

                <div
                    className={cn({
                        [styles.success]: type === 'success',
                        [styles.error]: type === 'error'
                    })}
                >

                    {children}
                </div>
            </div>
        </ClientOnlyPortal >
    )
}

最后在需要使用的地方引入使用alert:

import Tip from '../../components/tip/tip'
import { useEffect, useState } from 'react'
.
.
.
function Tools({ placeholder, postData }) {
    const [val, setVal] = useState(placeholder)
    let [tip, setTip] = useState({ message''type'' })
    useEffect(() => {
        if (tip.message) {
            // 3s后关闭弹窗
            let timer = setTimeout(() => {
                setTip({ message''type'' })
                clearTimeout(timer)
            }, 3000);
        }
    }, [tip.message])
    return (
        <div className={editStyles.tools}>
            <input type="text" placeholder={placeholder} value={val} onChange={e => setVal(e.target.value)} />
            <button onClick={async () => {
                const { title, sourceFile, date } = postData
                // 存储源文件
                if (!sourceFile || !title || !date || !val) return
                const res = await fetch('/api/__server__/savePost', {
                    method: 'post',
                    body: qs.stringify({ postId: val, isSaved: true, ...postData })
                })
                const data = await res.json()
                if (data && data.code === 0) {
                    setTip({ message: '保存成功!', type: 'success' })
                } else {
                    setTip({ message: `保存失败!${data.message}`, type: 'error' })
                }
            }}>保存</button>
            //  使用Tip
            {tip.message && <Tip type={tip.type}>{tip.message}</Tip>}

        </div>

    )
}


完成~