findDOMNode的替代 在 React 中获取真实 DOM 节点

在 React 中获取真实 DOM 节点:开发 EmptyGhost 组件

在 React 严格模式下,findDOMNode 方法会发出警告。因此,我设计了一个名为 EmptyGhost 的 React 组件,它允许我们优雅地获取真实的 DOM 节点,而不会引发任何警告。下面是我的开发过程。

背景

在 React 中,使用 ref 获取 DOM 节点是常见的需求。尤其是在操作事件、样式或子元素时,能够访问真实的 DOM 节点非常重要。然而,React 提供的 findDOMNode 方法由于一些潜在的性能问题,在严格模式下会发出警告。因此,我想通过创建自定义元素结合 React 的 forwardRefuseLayoutEffect 来实现这一功能。

组件实现

EmptyGhost 组件的核心目标是保持组件的透明性,同时让父组件能够获取到实际的 DOM 节点。为此,我使用了 customElements.define 来创建一个自定义元素,并通过 display: contents 确保它不会在布局中产生影响。

以下是完整的代码实现:

import * as React from 'react';

export interface EmptyGhostProps extends React.HTMLAttributes<HTMLDivElement> {
    is?: string,
    style?: Omit<React.CSSProperties, "display">
}

function assignment(node: HTMLDivElement | null, ref: React.ForwardedRef<Element | null | Text>) {
    const content = (node?.firstChild || null) as (Element | Text | null);
    typeof ref === 'function' ? ref(content) : ref && (ref.current = content);
}

customElements.define('empty-ghost', class Fragment extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
    }

    connectedCallback() {
        this.shadowRoot!.innerHTML = `
          <style>
            :host {
                display: contents !important;
            }
          </style>
          <slot></slot>
        `;
    }
});

const EmptyGhostRender: React.ForwardRefRenderFunction<Element | null | Text, EmptyGhostProps> = (props, ref) => {
    const contentsRef = React.useRef<HTMLDivElement | null>(null);

    React.useLayoutEffect(() => {
        const ob = new MutationObserver(() => {
            assignment(contentsRef.current!, ref);
        });
        ob.observe(contentsRef.current!, { childList: true });

        return () => {
            ob.disconnect();
        };
    }, [ref]);

    React.useLayoutEffect(() => {
        const styles = window.getComputedStyle(contentsRef.current!);
        if (styles.display !== 'contents') {
            console.warn(`[EmptyGhost]:`, contentsRef.current, `better an empty element`);
        }
    }, []);

    const { className, children, ...rest } = props;

    return React.createElement("empty-ghost",
        {
            ref: (node: HTMLDivElement | null) => {
                assignment(node, ref);
                contentsRef.current = node;
            },
            ...rest,
            class: className,
            style: {
                ...props.style,
                display: undefined
            },
        },
        children
    );
}

const EmptyGhost = React.forwardRef(EmptyGhostRender);
EmptyGhost.displayName = 'EmptyGhost';

export default EmptyGhost;

核心概念

  • 自定义元素 (customElements.define)
    我使用 customElements.define 创建了一个名为 empty-ghost 的自定义元素。这个元素最大的特点是使用了 display: contents,这使得它不会渲染为一个可见的容器,而仅作为一个内容占位符,避免破坏布局结构。
customElements.define('empty-ghost', class Fragment extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
    }

    connectedCallback() {
        this.shadowRoot!.innerHTML = `
          <style>
            :host {
                display: contents !important;
            }
          </style>
          <slot></slot>
        `;
    }
});

  • 使用 ref 获取真实 DOM 节点
    为了让父组件能够获取到 EmptyGhost 内的真实 DOM 节点,我使用了 React 的 forwardRef 和一个辅助函数 assignment。这个函数的作用是确保传入的 ref 可以引用到子元素的真实 DOM 节点,而不是自定义元素本身。
function assignment(node: HTMLDivElement | null, ref: React.ForwardedRef<Element | null | Text>) {
    const content = (node?.firstChild || null) as (Element | Text | null);
    typeof ref === 'function' ? ref(content) : ref && (ref.current = content);
}

  • 使用 MutationObserver 监听子元素变化
    在 EmptyGhost 的子元素发生变化时,我使用 MutationObserver 来监听 DOM 变动。这样,当子元素发生变化时,ref 会自动更新。
React.useLayoutEffect(() => {
    const ob = new MutationObserver(() => {
        assignment(contentsRef.current!, ref);
    });
    ob.observe(contentsRef.current!, { childList: true });

    return () => {
        ob.disconnect();
    };
}, [ref]);

  • display: contents 的使用
    为了确保 EmptyGhost 不会生成额外的 DOM 节点,我使用了 display: contents。这使得 EmptyGhost 的子元素直接参与布局,而不会被 empty-ghost 标签干扰。
React.useLayoutEffect(() => {
    const styles = window.getComputedStyle(contentsRef.current!);
    if (styles.display !== 'contents') {
        console.warn(`[EmptyGhost]:`, contentsRef.current, `better an empty element`);
    }
}, []);

总结

EmptyGhost 组件通过自定义元素与 React 的 forwardRef 实现了一种优雅的方式,让父组件能够引用真实 DOM 节点,同时避免了 React 严格模式下的警告。display: contents 确保组件不会生成多余的容器节点,保持了 DOM 的简洁性。

这个组件为在 React 项目中透明地操作 DOM 节点提供了灵活的解决方案,希望它能为你提供帮助和灵感。
最后附上npm链接 EmptyGhost

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值