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的逻辑里维护全局状态,追踪子元素的添加和移除:
- 创建计数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);
- 在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(...) */; };
- 修改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
相关产品推荐
相关产品推荐

