主题
createPortal:魔法传送门
更新: 10/14/2025字数: 0 字 时长: 0 分钟
一、什么是 Portal?
Portal 是 React 提供的一种将子节点渲染到父组件 DOM 层次结构之外的 DOM 节点中的方法。就像魔法世界的"传送门",它允许我们将组件渲染到 DOM 树的任意位置,同时保持其在 React 树中的逻辑位置。
注意
这是一个 API,不是组件,他的作用是:将一个组件渲染到 DOM 的任意位置,跟 Vue 的 Teleport 组件类似。
jsx
import { createPortal } from "react-dom";
function Modal() {
return createPortal(
<div className="modal">
<h2>我是模态框</h2>
<p>虽然我在这里定义,但我会出现在body末尾!</p>
</div>
document.body
);
}二、为什么需要 Portal?
1. 解决 CSS 层叠上下文问题
1,当父组件有 overflow: hidden 或 z-index 时,子组件可能被意外裁剪或遮盖。
2,解决position: fixed存在的一些问题。
2. 处理全局性 UI 元素
模态框、通知、工具、下拉框、全局 loading、提示、等需要突破容器限制。
3. 保持 DOM 结构合理性
将工具提示渲染到触发元素附近可能导致 DOM 结构混乱。
三、核心 API:createPortal
基本语法
jsx
createPortal(children, domNode, key?)children:任何可渲染的 React 子元素domNode:已经存在的 DOM 节点key(可选):用作 portal 的 key
完整示例
jsx
function Tooltip({ children, targetId }) {
const [isVisible, setIsVisible] = useState(false);
const targetElement = document.getElementById(targetId);
if (!targetElement) return null;
return (
<>
{createPortal(
isVisible && (
<div className="tooltip">
{children}
</div>
),
targetElement
)}
</>
);
}
// 使用
<Tooltip targetId="btn-1">这是一个提示</Tooltip>
<button id="btn-1">悬停我</button>四、Portal 的关键特性
1. 事件冒泡机制
虽然 DOM 结构不同,但事件仍然按照 React 树的结构冒泡
jsx
function Parent() {
const handleClick = () => {
console.log("点击事件从Portal冒泡上来了!");
};
return (
<div onClick={handleClick}>
<p>父组件</p>
<Modal /> {/* 使用createPortal渲染到body */}
</div>
);
}2. 生命周期与上下文
Portal 组件完全保留 React 上下文和生命周期
jsx
const ThemeContext = createContext("light");
function ThemedModal() {
const theme = useContext(ThemeContext);
useEffect(() => {
console.log("Modal mounted");
return () => console.log("Modal unmounted");
}, []);
return createPortal(<div className={`modal ${theme}`}>...</div>, document.body);
}五、实战应用场景
1. 模态对话框实现
jsx
function Modal({ children, onClose }) {
const modalRoot = useMemo(() => document.createElement("div"), []);
useEffect(() => {
document.body.appendChild(modalRoot);
return () => document.body.removeChild(modalRoot);
}, [modalRoot]);
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
modalRoot
);
}2. 全局通知系统
jsx
const notificationRoot = document.getElementById("notifications");
function Notification({ message }) {
return createPortal(<div className="notification">{message}</div>, notificationRoot);
}六、性能优化与最佳实践
1. 复用 DOM 节点
jsx
const portalRoot = document.getElementById("portal-root");
function MyPortal({ children }) {
// 使用useMemo避免重复创建节点
const container = useMemo(() => document.createElement("div"), []);
useEffect(() => {
portalRoot.appendChild(container);
return () => portalRoot.removeChild(container);
}, [container]);
return createPortal(children, container);
}2. 避免内存泄漏
确保在组件卸载时清理 Portal 节点
jsx
useEffect(() => {
const div = document.createElement("div");
document.body.appendChild(div);
return () => {
document.body.removeChild(div);
};
}, []);3. SSR 兼容性处理
jsx
function SafePortal({ children }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return createPortal(children, document.body);
}七、特别注意
推荐使用 createPortal 因为他更灵活,可以挂载到任意位置,而position: fixed,会有很多问题,在默认的情况下他是根据浏览器视口进行定位的,但是如果父级设置了transform、perspective、filter 或 backdrop-filter 属性非 none 时,他就会相对于父级进行定位,这样就会导致 Modal 组件定位不准确(他不是一定按照浏览器视口进行定位),所以不推荐使用。