React useLayoutEffect в дочернем элементе портала запускается до рендеринга компонента

Я использую компонент портала для создания портала и добавления его в документ. Какой-то компонент (подсказка, в моем случае использования) является дочерним элементом портала и должен считать его ширину (в моем случае, чтобы проверить, будет ли всплывающая подсказка за пределами окна, и при необходимости переместить ее), а затем повторно -рендерить себя с помощью 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
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
0
0
27
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Обычное поведение этого хука — рендеринг первым.

useLayoutEffect является синхронным, тогда как useEffect является асинхронным. так как это так, это действительно зависит от того, где вы поместите этот крючок.

Документы

Дело не в этом, за пределами портала все работает нормально, а внутри портала не работает.

notme1560 10.05.2022 06:24
Ответ принят как подходящий

Портал предназначен для отображения компонента на самой вершине дерева 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 и вызовет вспышку, но это работает отлично.

notme1560 10.05.2022 08:32

Другие вопросы по теме