如何通过组件Ref而非容器ID创建React Portal,实现无ID赋值方案
基于容器ref实现React Portal挂载的方案
根因分析
你写的代码不生效的核心原因是:ref.current 的赋值发生在React的DOM提交阶段,也就是组件渲染完成、真实DOM挂载到文档之后才会执行。而你在组件渲染阶段(return createPortal的逻辑执行时)就直接读取ref.current,此时它的值还是null,不符合createPortal第二个参数必须是真实DOM节点的要求。
完全可以不用ID,也不用额外创建DOM节点,只需要等容器ref对应的DOM节点挂载完成后再渲染Portal即可,实现方案如下:
推荐实现方式
建议将容器ref作为普通props传入(更符合语义,forwardRef一般用于向父组件暴露当前组件的DOM节点),配合useEffect监听ref的状态来触发Portal渲染:
组件实现代码
import React, { useState, useEffect } from 'react'; import ReactDOM from 'react-dom'; // 组件入参类型定义 type Props = { state: { open: boolean }; options: React.ReactNode; dispatch: () => void; // 父组件传入的目标容器ref containerRef: React.RefObject<HTMLElement>; }; const RefPortal: React.FC<Props> = (props) => { const { state, options, containerRef } = props; // 存储已挂载的容器DOM节点 const [mountContainer, setMountContainer] = useState<HTMLElement | null>(null); useEffect(() => { // 容器DOM挂载完成后更新状态,触发Portal重渲染 if (containerRef.current) { setMountContainer(containerRef.current); } // 容器卸载时清理 return () => setMountContainer(null); }, [containerRef]); // 容器未就绪或者弹窗未开启时不渲染 if (!state.open || !mountContainer) return null; return ReactDOM.createPortal( <div style={{ height: '140px', background: 'white', overflowY: 'scroll', position: 'absolute', width: '100%', }} > {options} </div>, mountContainer ); }; export default RefPortal;
父组件调用示例
import React, { useRef, useState } from 'react'; import RefPortal from './RefPortal'; const Parent = () => { // 绑定到目标容器的ref const containerRef = useRef<HTMLDivElement>(null); const [portalState, setPortalState] = useState({ open: true }); return ( <div> {/* 你要挂载Portal的目标容器,注意如果Portal内容是absolute定位,容器最好设置relative */} <div ref={containerRef} style={{ position: 'relative', height: '200px', border: '1px solid #eee' }} /> {/* 传入容器ref */} <RefPortal state={portalState} options={<div>选项1<br/>选项2<br/>选项3</div>} dispatch={() => {}} containerRef={containerRef} /> </div> ); }; export default Parent;
如果你坚持要使用forwardRef接收容器ref
只需要额外处理ref可能是回调类型的情况即可,实现代码如下:
import React, { useState, useEffect, forwardRef } from 'react'; import ReactDOM from 'react-dom'; type Props = { state: { open: boolean }; options: React.ReactNode; dispatch: () => void; }; const RefPortal = forwardRef<HTMLElement, Props>((props, ref) => { const { state, options } = props; const [mountContainer, setMountContainer] = useState<HTMLElement | null>(null); useEffect(() => { let node: HTMLElement | null = null; if (typeof ref === 'object' && ref !== null) { node = ref.current; } if (node) { setMountContainer(node); } return () => setMountContainer(null); }, [ref]); if (!state.open || !mountContainer) return null; return ReactDOM.createPortal( <div style={{ height: '140px', background: 'white', overflowY: 'scroll', position: 'absolute', width: '100%', }} > {options} </div>, mountContainer ); }); export default RefPortal;
内容的提问来源于stack exchange,提问作者kenshin
相关产品推荐
相关产品推荐

