Я использую компонент портала для создания портала и добавления его в документ.
Какой-то компонент (подсказка, в моем случае использования) является дочерним элементом портала и должен считать его ширину (в моем случае, чтобы проверить, будет ли всплывающая подсказка за пределами окна, и при необходимости переместить ее), а затем повторно -рендерить себя с помощью 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.info("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 является асинхронным. так как это так, это действительно зависит от того, где вы поместите этот крючок.
Портал предназначен для отображения компонента на самой вершине дерева DOM и не зависит от родительских компонентов компонента портала. Вот почему он не может изначально рассчитывать размеры, которые обычно зависят от контейнеров (родительских компонентов).
Для получения размеров элементов в порталах вы можете использовать setTimeout
, который поможет вам выполнить вашу логику после выполнения всех остальных стеки вызовов (в ваших случаях стеки вызовов предназначены для инициализации порталов)
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.info("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" />
</>
);
}
Вы можете проверить эта песочница для тестирования.
Большое спасибо! Я был обеспокоен тем, что тайм-аут нарушит синхронный характер useLayoutEffect и вызовет вспышку, но это работает отлично.
Дело не в этом, за пределами портала все работает нормально, а внутри портала не работает.