React useLayoutEffect 在组件呈现之前运行的门户子项中

React useLayoutEffect in a child of a portal runs before the component renders

我正在使用门户组件创建门户并将其添加到文档中。 某些组件(工具提示,在我的实际用例中)是门户的子项,需要读取其宽度(在我的例子中,检查工具提示是否在 window 之外,并在必要时重新定位) ,然后使用 useLayoutEffect.

重新渲染自身

我的问题是在渲染元素之前调用了 useLayoutEffect 的回调,因此当我执行类似 getBoundingClientRect() 的操作时,宽度仍然为 0。如果我将组件移出门户,则它工作正常。

https://codesandbox.io/s/divine-snowflake-071dbw

相关部分:

const Portal = ({ children }) => {
  const [container] = useState(() => {
    const el = document.createElement("div");
    return el;
  });

  useEffect(() => {
    document.body.appendChild(container);
    return () => {
      document.body.removeChild(container);
    };
  }, [container]);

  return createPortal(children, container);
};

const MyComponent = ({ name }) => {
  const ref = useRef(null);

  useLayoutEffect(() => {
    if (ref.current) {
      const rect = ref.current.getBoundingClientRect();
      // For the component inside the portal,
      // the width/height/etc is all 0
      console.log("rect of", name, rect.width);
    }
  }, [name]);

  return <div ref={ref}>{name}</div>;
};

const App = () => {
  return (
    <>
      <Portal>
        <MyComponent name="component inside portal" />
      </Portal>
      <MyComponent name="outside" />
    </>
  );
}

日志

rect of component inside portal 0
rect of outside 770

这个钩子的正常行为是先渲染。

useLayoutEffect 是同步的,useEffect 是异步的。 既然是这样,那真的取决于你把那个钩子放在哪里。

Docs

The portal 是在 DOM 树的最顶端呈现组件,不依赖于门户组件的父组件。这就是为什么它最初无法计算通常取决于容器(父组件)的大小的原因。

要获取门户中的元素大小,您可以使用setTimeout that will help you to execute your logic after all other call stacks完成(在您的情况下,调用堆栈用于初始化门户)

import { useState, useEffect, useLayoutEffect, useRef } from "react";
import { createPortal } from "react-dom";

const Portal = ({ children }) => {
  const [container] = useState(() => {
    const el = document.createElement("div");
    return el;
  });

  useEffect(() => {
    document.body.appendChild(container);
    return () => {
      document.body.removeChild(container);
    };
  }, [container]);

  return createPortal(children, container);
};

const MyComponent = ({ name }) => {
  const ref = useRef(null);

  useLayoutEffect(() => {
    if (ref.current) {
      //delay the execution till the portal initialization completed
      setTimeout(() => {
        const rect = ref.current.getBoundingClientRect();
      console.log("rect of", name, rect.width);
      })
    }
  }, [name]);

  return <div ref={ref}>{name}</div>;
};

export default function App() {
  return (
    <>
      <Portal>
        <MyComponent name="component inside portal" />
      </Portal>
      <MyComponent name="outside" />
    </>
  );
}

您可以查看 this sandbox 进行测试。