You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过组件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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.28 12:45:00