RootRenderer Portal 渲染器
将子元素渲染到 document.body 的 Portal 组件,用于模态框、提示等需要脱离文档流的场景。
导入
ts
import { RootRenderer } from '@xcloud/ui-mobile';示例
基础用法
tsx
import { RootRenderer } from '@xcloud/ui-mobile';
function Modal({ children }) {
return (
<RootRenderer>
<div className="fixed inset-0 bg-black/50 flex items-center justify-center">
{children}
</div>
</RootRenderer>
);
}API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| children | ReactNode | - | 要渲染的子元素 |
实现原理
RootRenderer 是 React Portal 的简单封装:
tsx
export const RootRenderer = ({ children }: RootRendererProps) => {
// SSR 安全检查
if (!canUseDOM || !document.body) {
return isValidElement(children) ? children : null;
}
// 使用 Portal 渲染到 body
return createPortal(children, document.body);
};SSR 支持
- 服务端渲染时,
canUseDOM为false - 降级为普通渲染,避免
document引用错误 - 客户端水合后自动切换为 Portal 渲染
使用场景
1. 模态对话框
tsx
function Dialog({ open, onClose, children }) {
if (!open) return null;
return (
<RootRenderer>
<div className="fixed inset-0 z-50">
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
<div className="relative z-10 flex items-center justify-center min-h-full">
{children}
</div>
</div>
</RootRenderer>
);
}2. 全局提示
tsx
function Toast({ message, type }) {
return (
<RootRenderer>
<div className="fixed top-4 right-4 z-[9999]">
<div className={`px-4 py-3 rounded-lg shadow-lg ${
type === 'success' ? 'bg-green-500' : 'bg-red-500'
} text-white`}>
{message}
</div>
</div>
</RootRenderer>
);
}3. Drawer 抽屉
tsx
function Drawer({ open, side = 'right', children }) {
if (!open) return null;
return (
<RootRenderer>
<div className="fixed inset-0 z-40">
<div className="absolute inset-0 bg-black/30" />
<div className={`absolute top-0 bottom-0 bg-white shadow-xl ${
side === 'right' ? 'right-0' : 'left-0'
} w-80`}>
{children}
</div>
</div>
</RootRenderer>
);
}4. 全局加载指示器
tsx
function GlobalLoader({ loading }) {
if (!loading) return null;
return (
<RootRenderer>
<div className="fixed inset-0 z-[10000] flex items-center justify-center bg-black/20">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-blue-500 border-t-transparent" />
</div>
</RootRenderer>
);
}优势
1. 避免样式冲突
普通渲染可能受到父容器样式影响:
tsx
{/* ❌ 可能被裁剪 */}
<div className="overflow-hidden relative">
<Modal /> {/* 内容可能被 overflow: hidden 裁剪 */}
</div>
{/* ✅ 不受父容器影响 */}
<div className="overflow-hidden relative">
<RootRenderer>
<Modal /> {/* 渲染到 body,不会被裁剪 */}
</RootRenderer>
</div>2. z-index 层级
tsx
{/* ❌ 可能被其他元素遮挡 */}
<div className="z-10">
<Modal className="z-50" /> {/* 仍然受父容器 z-10 限制 */}
</div>
{/* ✅ 独立层级 */}
<RootRenderer>
<Modal className="z-50" /> {/* 真正的 z-50,不受父容器影响 */}
</RootRenderer>3. 事件冒泡控制
Portal 元素虽然 DOM 位置在 body,但 React 事件冒泡遵循组件树:
tsx
<div onClick={handleParentClick}>
<RootRenderer>
<div onClick={handleChildClick}>
{/* 点击这里会先触发 handleChildClick,再触发 handleParentClick */}
</div>
</RootRenderer>
</div>与直接使用 createPortal 的区别
| 特性 | RootRenderer | createPortal |
|---|---|---|
| SSR 安全 | ✅ 内置检查 | ❌ 需要手动处理 |
| 代码简洁 | ✅ 一行代码 | ⚠️ 需要检查 canUseDOM |
| 目标容器 | 固定为 body | 可自定义 |
| 使用场景 | 通用场景 | 需要自定义容器时 |
最佳实践
条件渲染: 在 Portal 外部控制,避免不必要的渲染
tsx{open && <RootRenderer><Modal /></RootRenderer>} // ✅ 好 <RootRenderer>{open && <Modal />}</RootRenderer> // ⚠️ 可以,但会有空 Portal焦点管理: Portal 内的元素需要手动管理焦点
tsxuseEffect(() => { if (open) { const firstInput = modalRef.current?.querySelector('input'); firstInput?.focus(); } }, [open]);无障碍性: 添加适当的 ARIA 属性
tsx<RootRenderer> <div role="dialog" aria-modal="true" aria-labelledby="modal-title" > <h2 id="modal-title">标题</h2> </div> </RootRenderer>清理: 组件卸载时 Portal 会自动清理,无需手动处理
性能优化
懒加载: 对于不常用的 Portal 组件,使用 React.lazy
tsxconst Modal = lazy(() => import('./Modal')); {showModal && ( <Suspense fallback={null}> <RootRenderer> <Modal /> </RootRenderer> </Suspense> )}避免重复渲染: 使用 memo 包装 Portal 内容
tsxconst ModalContent = memo(({ data }) => <div>{data}</div>); <RootRenderer> <ModalContent data={data} /> </RootRenderer>
常见问题
Q: RootRenderer 与 Snackbar 的关系?
A: Snackbar 内部使用 RootRenderer 实现 Portal 渲染:
tsx
export const Snackbar = ({ children, ...props }) => {
return (
<RootRenderer>
<div className="fixed bottom-4 left-4 right-4">
{children}
</div>
</RootRenderer>
);
};Q: 可以自定义目标容器吗?
A: RootRenderer 固定渲染到 body。如需自定义容器,直接使用 createPortal:
tsx
import { createPortal } from 'react-dom';
const customContainer = document.getElementById('portal-root');
return createPortal(children, customContainer);Q: SSR 时如何处理样式?
A: 服务端渲染时 RootRenderer 会降级为普通渲染,确保样式正确:
tsx
// 在服务端和客户端都能正常工作
<RootRenderer>
<div className="modal">...</div> {/* 样式会正确应用 */}
</RootRenderer>浏览器兼容性
- ✅ 所有现代浏览器
- ✅ IE11+ (支持 React Portal)
- ✅ SSR 环境 (降级为普通渲染)
相关组件
源码参考
完整实现仅 10 行代码,极其简洁:
tsx
import { isValidElement, ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { canUseDOM } from '@xcloud/ui-core/utils';
export interface RootRendererProps {
children?: ReactNode;
}
export const RootRenderer = ({ children }: RootRendererProps) => {
if (!canUseDOM || !document.body) {
return isValidElement(children) ? children : null;
}
return createPortal(children, document.body);
};