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

React Portal下ToolkitArea子元素统计异常及按需显示问题

问题与解决方案

需求

仅当ID为#toolkitArea的容器存在子元素时,显示ToolkitArea组件。

问题

无法正确统计ToolkitArea容器内当前的子元素数量。

已实施方案

我有一个Toolbar组件,希望通过React Portal添加按钮时显示该组件。当前实现思路是在ToolkitArea组件内使用useEffect钩子,触发时检查容器内的子元素数量,代码如下:

import {  faEyeSlash, faEye } from '@fortawesome/free-solid-svg-icons'
import ToolkitButton from './ToolkitButton'
import { useState } from 'react'
export interface ToolkitAreaProps {
  children?: React.ReactNode

}
const ToolkitArea = ({ children }:ToolkitAreaProps) => {
  const [visible, setVisible] = useState<boolean>(true)
  const [isEmpty, setEmpty] = useState<boolean>(false)
  const toolkit = useRef<HTMLDivElement>(null)
  useEffect(() => {
    const toolkitArea = (document.getElementById('toolkitArea') as HTMLDivElement)
    console.log(toolkitArea.children, '', toolkit.current?.children, toolkit.current?.children.length)
    if(toolkitArea.children.length > 0)
      setEmpty(false)
    else setEmpty(true)
  }, [toolkit])

  return <div className="fixed bottom-6 w-full h-16 z-40 px-8 md:pl-32 md:pr-16 flex">
    <div className={`-ml-4 md:-ml-12 rounded-md w-16 h-16 bg-indigo-700 flex items-center justify-center shadow shadow-black shadow-md ${(visible || isEmpty) ? 'invisible' : 'visible'}`}>
      <ToolkitButton icon={faEye} onClick={() => setVisible(true)} tooltip={'Show Toolbar'}/>
    </div>
    <div className={`h-full mx-auto bg-indigo-700 w-fit max-w-full rounded-md shadow shadow-black shadow-md py-2 flex px-4 overflow-x-auto md:overflow-x-visible ${(visible || isEmpty) ? 'invisible' : 'visible'}`}>
      <ToolkitButton icon={faEyeSlash} onClick={() => setVisible(false)} className="-order-1" tooltip={'Toggle Toolbar'}/>
      <div ref={toolkit} className={'w-fit flex gap-4 h-full pl-4'} id={'toolkitArea'}>

      </div>
    </div>
  </div>
} 

export default ToolkitArea

浏览器控制台显示,即使页面上已经有5个元素,children.length还是0。推测是按钮在useEffect触发后才被添加,导致日志里的内容被动态更新了。

解决方案

方法1:通过React状态追踪子元素数量

既然是用React Portal添加子元素,直接在Portal的逻辑里维护全局状态,追踪子元素的添加和移除:

  1. 创建计数Context
import { createContext, useContext, useState, ReactNode } from 'react';

const ToolkitCountContext = createContext({
  count: 0,
  increment: () => {},
  decrement: () => {}
});

export const ToolkitCountProvider = ({ children }: { children: ReactNode }) => {
  const [count, setCount] = useState(0);
  
  const increment = () => setCount(prev => prev + 1);
  const decrement = () => setCount(prev => Math.max(0, prev - 1));
  
  return (
    <ToolkitCountContext.Provider value={{ count, increment, decrement }}>
      {children}
    </ToolkitCountContext.Provider>
  );
};

export const useToolkitCount = () => useContext(ToolkitCountContext);
  1. 在Portal组件中更新计数
import { useToolkitCount } from './ToolkitCountContext';
import { useEffect } from 'react';

const ToolkitPortalButton = ({ children }) => {
  const { increment, decrement } = useToolkitCount();
  
  useEffect(() => {
    increment();
    return () => decrement(); // 组件卸载时减少计数
  }, [increment, decrement]);
  
  // 你的Portal渲染逻辑
  return /* 这里写Portal的代码,比如ReactDOM.createPortal(...) */;
};
  1. 修改ToolkitArea组件使用计数
import { useToolkitCount } from './ToolkitCountContext';

const ToolkitArea = ({ children }:ToolkitAreaProps) => {
  const [visible, setVisible] = useState<boolean>(true);
  const { count } = useToolkitCount();
  const isEmpty = count === 0;

  return (
    <div className="fixed bottom-6 w-full h-16 z-40 px-8 md:pl-32 md:pr-16 flex">
      <div className={`-ml-4 md:-ml-12 rounded-md w-16 h-16 bg-indigo-700 flex items-center justify-center shadow shadow-black shadow-md ${(visible || isEmpty) ? 'invisible' : 'visible'}`}>
        <ToolkitButton icon={faEye} onClick={() => setVisible(true)} tooltip={'显示工具栏'}/>
      </div>
      <div className={`h-full mx-auto bg-indigo-700 w-fit max-w-full rounded-md shadow shadow-black shadow-md py-2 flex px-4 overflow-x-auto md:overflow-x-visible ${(visible || isEmpty) ? 'invisible' : 'visible'}`}>
        <ToolkitButton icon={faEyeSlash} onClick={() => setVisible(false)} className="-order-1" tooltip={'切换工具栏'}/>
        <div ref={toolkit} className={'w-fit flex gap-4 h-full pl-4'} id={'toolkitArea'}>
          {children}
        </div>
      </div>
    </div>
  );
};

方法2:用MutationObserver监听容器变化

如果不想用状态管理,直接监听DOM变化:

const ToolkitArea = ({ children }:ToolkitAreaProps) => {
  const [visible, setVisible] = useState<boolean>(true);
  const [isEmpty, setEmpty] = useState<boolean>(true);
  const toolkit = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new MutationObserver(() => {
      const hasChildren = toolkit.current?.children.length > 0;
      setEmpty(!hasChildren);
    });

    if (toolkit.current) {
      observer.observe(toolkit.current, {
        childList: true, // 监听子元素的添加/移除
        subtree: false
      });
    }

    return () => observer.disconnect(); // 组件卸载时停止监听
  }, []);

  return (
    <div className="fixed bottom-6 w-full h-16 z-40 px-8 md:pl-32 md:pr-16 flex">
      <div className={`-ml-4 md:-ml-12 rounded-md w-16 h-16 bg-indigo-700 flex items-center justify-center shadow shadow-black shadow-md ${(visible || isEmpty) ? 'invisible' : 'visible'}`}>
        <ToolkitButton icon={faEye} onClick={() => setVisible(true)} tooltip={'显示工具栏'}/>
      </div>
      <div className={`h-full mx-auto bg-indigo-700 w-fit max-w-full rounded-md shadow shadow-black shadow-md py-2 flex px-4 overflow-x-auto md:overflow-x-visible ${(visible || isEmpty) ? 'invisible' : 'visible'}`}>
        <ToolkitButton icon={faEyeSlash} onClick={() => setVisible(false)} className="-order-1" tooltip={'切换工具栏'}/>
        <div ref={toolkit} className={'w-fit flex gap-4 h-full pl-4'} id={'toolkitArea'}>
          {children}
        </div>
      </div>
    </div>
  );
};

原代码问题分析

之前的useEffect依赖是toolkit,但ref的引用本身不会变化,所以useEffect只在组件挂载时执行一次。而Portal的子元素是挂载后才插入的,因此第一次检查时子元素数量为0,后续不会再触发检查。

内容的提问来源于stack exchange,提问作者Cascade

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 04:54:54