在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>
)
}
完成~