У меня есть компонент реагирования, который должен знать свои размеры заранее, до того, как он отобразится сам.

Когда я создавал виджет в jquery, я мог просто $('#container').width() и получить ширину контейнера заранее, когда я собрал свой компонент.

<div id='container'></div>

Размеры этих контейнеров определены в CSS вместе с кучей других контейнеров на странице. кто определяет высоту, ширину и расположение компонентов в React? Я привык к тому, что CSS делает это и может получить к нему доступ. Но в React кажется, что я могу получить доступ к этой информации только после рендеринга компонента.

77
Shai UI 2 Мар 2018 в 00:22

6 ответов

Лучший ответ

Как уже упоминалось, вы не можете получить размеры элемента, пока он не будет отображен в DOM. В React вы можете визуализировать только элемент контейнера, затем получить его размер в componentDidMount, а затем отобразить остальную часть содержимого.

Я сделал рабочий пример.

Обратите внимание, что использование setState в componentDidMount является анти-паттерном, но в данном случае это нормально, так как это именно то, чего мы пытаемся достичь.

Ура !

Код:

import React, { Component } from 'react';

export default class Example extends Component {
  state = {
    dimensions: null,
  };

  componentDidMount() {
    this.setState({
      dimensions: {
        width: this.container.offsetWidth,
        height: this.container.offsetHeight,
      },
    });
  }

  renderContent() {
    const { dimensions } = this.state;

    return (
      <div>
        width: {dimensions.width}
        <br />
        height: {dimensions.height}
      </div>
    );
  }

  render() {
    const { dimensions } = this.state;

    return (
      <div className="Hello" ref={el => (this.container = el)}>
        {dimensions && this.renderContent()}
      </div>
    );
  }
}
29
Stanko 1 Мар 2018 в 21:40

Решение @ Stanko приятно и лаконично, но пост-рендер. У меня другой сценарий, рендеринг элемента <p> внутри SVG <foreignObject> (в диаграмме Recharts). <p> содержит текст, который переносится, и окончательную высоту ограниченного по ширине <p> трудно предсказать. <foreignObject> в основном является окном просмотра, и если он будет слишком длинным, он будет блокировать щелчки / касания к основным элементам SVG, слишком короткий и отрубит нижнюю часть <p>. Мне нужна плотная посадка, собственная высота, определяемая стилем DOM, перед рендерингом React. Также нет JQuery.

Таким образом, в моем функциональном компоненте React я создаю фиктивный узел <p>, помещаю его в действующую модель DOM вне клиентской области просмотра документа, измеряю ее и снова удаляю. Затем используйте это измерение для <foreignObject>.

[Отредактировано методом с использованием классов CSS] [Отредактировано: Firefox ненавидит findCssClassBySelector, пока застрявший с жестким кодированием.]

const findCssClassBySelector = selector => [...document.styleSheets].reduce((el, f) => {
  const peg = [...f.cssRules].find(ff => ff.selectorText === selector);
  if(peg) return peg; else return el;
}, null);

// find the class
const eventLabelStyle = findCssClassBySelector("p.event-label")

// get the width as a number, default 120
const eventLabelWidth = eventLabelStyle && eventLabelStyle.style ? parseInt(eventLabelStyle.style.width) : 120

const ALabel = props => {
  const {value, backgroundcolor: backgroundColor, bordercolor: borderColor, viewBox: {x, y}} = props

  // create a test DOM node, place it out of sight and measure its height
  const p = document.createElement("p");
  p.innerText = value;
  p.className = "event-label";
  // out of sight
  p.style.position = "absolute";
  p.style.top = "-1000px";
  // // place, measure, remove
  document.body.appendChild(p);
  const {offsetHeight: calcHeight} = p; // <<<< the prize
  // does the DOM reference to p die in garbage collection, or with local scope? :p

  document.body.removeChild(p);
  return <foreignObject {...props} x={x - eventLabelWidth / 2} y={y} style={{textAlign: "center"}} width={eventLabelWidth} height={calcHeight} className="event-label-wrapper">
    <p xmlns="http://www.w3.org/1999/xhtml"
       className="event-label"
       style={{
         color: adjustedTextColor(backgroundColor, 125),
         backgroundColor,
         borderColor,
       }}
    >
      {value}
    </p>
  </foreignObject>
}

Ужасно, много предположений, вероятно, медленно, и я нервничаю по поводу мусора, но это работает. Обратите внимание, что ширина реквизита должна быть числом.

0
dkloke 6 Дек 2019 в 07:48

С подходом @ shane для обработки изменения размера окна возникает неожиданная «ошибка»: функциональный компонент добавляет новый прослушиватель событий при каждом повторном рендеринге и никогда не удаляет прослушиватель событий, поэтому число слушателей событий растет экспоненциально с каждым изменением размера. Вы можете увидеть это, регистрируя каждый вызов window.addEventListener:

window.addEventListener("resize", () => {
  console.log(`Resize: ${dimensions.width} x ${dimensions.height}`);
  clearInterval(movement_timer);
  movement_timer = setTimeout(test_dimensions, RESET_TIMEOUT);
});

Это можно исправить с помощью шаблона очистки событий. Вот некоторый код, представляющий собой смесь кода @ shane и этого учебное пособие с логикой изменения размера в пользовательском хуке:

/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect, useLayoutEffect, useRef } from "react";

// Usage
function App() {
  const targetRef = useRef();
  const size = useDimensions(targetRef);

  return (
    <div ref={targetRef}>
      <p>{size.width}</p>
      <p>{size.height}</p>
    </div>
  );
}

// Hook
function useDimensions(targetRef) {
  const getDimensions = () => {
    return {
      width: targetRef.current ? targetRef.current.offsetWidth : 0,
      height: targetRef.current ? targetRef.current.offsetHeight : 0
    };
  };

  const [dimensions, setDimensions] = useState(getDimensions);

  const handleResize = () => {
    setDimensions(getDimensions());
  };

  useEffect(() => {
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  useLayoutEffect(() => {
    handleResize();
  }, []);
  return dimensions;
}

export default App;

здесь есть рабочий пример.

Этот код не использует таймер для простоты, но этот подход более подробно обсуждается в связанном руководстве.

0
RobJacobson 14 Фев 2020 в 03:00

Как уже говорилось, это ограничение браузеров - они визуализируются за один раз и «в одном потоке» (с точки зрения JS) между вашим скриптом, который манипулирует DOM, и между выполнением обработчиков событий. Чтобы получить размеры после манипулирования / загрузки DOM, вам нужно уступить (оставить свою функцию) и позволить браузеру рендериться, и реагировать на какое-то событие, когда рендеринг выполняется.

Но попробуйте этот трюк:
Вы можете попытаться установить CSS display: hidden; position: absolute; и ограничить его неким невидимым ограничивающим прямоугольником, чтобы получить желаемую ширину. Затем уступите, и когда рендеринг будет завершен, вызовите $('#container').width().

Идея такова: поскольку display: hidden заставляет элемент занимать пространство, которое он занимал бы, если бы был видим, вычисления должны выполняться в фоновом режиме. Я не уверен, что это квалифицируется как «перед рендерингом».


Отказ от ответственности :
Я не пробовал, поэтому дайте мне знать, если это сработало.
И я не уверен, как это будет сочетаться с React.

4
Ondra Žižka 27 Июл 2018 в 09:48

Тебе нельзя. Во всяком случае, ненадежно. Это ограничение поведения браузера в целом, а не React.

Когда вы вызываете $('#container').width(), вы запрашиваете ширину элемента, который визуализировал в DOM. Даже в jQuery вы не можете обойти это.

Если вам абсолютно необходима ширина элемента перед тем, как он рендерит , вам необходимо оценить его. Если вам нужно измерить до того, как вы видимы , вы можете сделать это, применяя visibility: hidden, или визуализировать его где-то дискретно на странице, а затем переместить после измерения.

6
Litty 1 Мар 2018 в 21:29

В приведенном ниже примере используется ловушка реакции useEffect .

Рабочий пример здесь

import React, { useRef, useLayoutEffect, useState } from "react";

const ComponentWithDimensions = props => {
  const targetRef = useRef();
  const [dimensions, setDimensions] = useState({ width:0, height: 0 });

  useLayoutEffect(() => {
    if (targetRef.current) {
      setDimensions({
        width: targetRef.current.offsetWidth,
        height: targetRef.current.offsetHeight
      });
    }
  }, []);

  return (
    <div ref={targetRef}>
      <p>{dimensions.width}</p>
      <p>{dimensions.height}</p>
    </div>
  );
};

export default ComponentWithDimensions;

Некоторые предостережения

useEffect не сможет определить свое влияние на ширину и высоту

Например, если вы измените хук состояния без указания начальных значений (например, const [dimensions, setDimensions] = useState({});), высота будет считаться нулевой при визуализации, потому что

  • не установлена явная высота для компонента через CSS
  • только контент, нарисованный до использования, может использоваться для измерения ширины и высоты.
  • Единственное содержимое компонента - это теги p с переменными высоты и ширины, когда пустое значение даст компоненте высоту ноль
  • useEffect больше не будет запускаться после установки новых переменных состояния.

Это, вероятно, не проблема в большинстве случаев использования, но я подумал, что включу это, потому что это имеет значение для изменения размера окна.

Изменение размера окна

Я также думаю, что в первоначальном вопросе есть некоторые неисследованные последствия. Я столкнулся с проблемой изменения размера окна для динамически нарисованных компонентов, таких как диаграммы.

Я включил этот ответ, хотя он не был указан , потому что

  1. Справедливо предположить, что если размеры нужны приложению, они, вероятно, понадобятся при изменении размера окна.
  2. Только изменения состояния или реквизита вызовут перерисовку, поэтому также необходим слушатель изменения размера окна для отслеживания изменений размеров
  3. Производительность снижается, если перерисовывать компонент при каждом изменении размера окна , добавляя более сложные компоненты. я нашел помогло введение setTimeout и clearInterval. Мой компонент включил диаграмму, так что мой процессор взлетел и браузер начал ползать. Решение ниже исправило это для меня.

Код ниже, рабочий пример здесь

import React, { useRef, useLayoutEffect, useState } from 'react';

const ComponentWithDimensions = (props) => {
    const targetRef = useRef();
    const [dimensions, setDimensions] = useState({});

    // holds the timer for setTimeout and clearInterval
    let movement_timer = null;

    // the number of ms the window size must stay the same size before the
    // dimension state variable is reset
    const RESET_TIMEOUT = 100;

    const test_dimensions = () => {
      // For some reason targetRef.current.getBoundingClientRect was not available
      // I found this worked for me, but unfortunately I can't find the
      // documentation to explain this experience
      if (targetRef.current) {
        setDimensions({
          width: targetRef.current.offsetWidth,
          height: targetRef.current.offsetHeight
        });
      }
    }

    // This sets the dimensions on the first render
    useLayoutEffect(() => {
      test_dimensions();
    }, []);

    // every time the window is resized, the timer is cleared and set again
    // the net effect is the component will only reset after the window size
    // is at rest for the duration set in RESET_TIMEOUT.  This prevents rapid
    // redrawing of the component for more complex components such as charts
    window.addEventListener('resize', ()=>{
      clearInterval(movement_timer);
      movement_timer = setTimeout(test_dimensions, RESET_TIMEOUT);
    });

    return (
      <div ref={ targetRef }>
        <p>{ dimensions.width }</p>
        <p>{ dimensions.height }</p>
      </div>
    );
}

export default ComponentWithDimensions;

re: таймаут изменения размера окна . В моем случае я рисую панель мониторинга с графиками ниже этих значений и обнаружил, что 100 мс RESET_TIMEOUT показались удачными баланс для меня между использованием процессора и отзывчивостью. У меня нет объективных данных о том, что идеально, поэтому я сделал это переменной.

39
Shane 31 Июл 2019 в 15:25