Как я могу реагировать на ширину элемента DOM с автоматическим размером в React?

86

У меня сложная веб-страница с использованием компонентов React, и я пытаюсь преобразовать страницу из статического макета в более отзывчивый макет с изменяемым размером. Однако я продолжаю сталкиваться с ограничениями в React, и мне интересно, есть ли стандартный шаблон для решения этих проблем. В моем конкретном случае у меня есть компонент, который отображается как div с display: table-cell и width: auto.

К сожалению, я не могу запросить ширину своего компонента, потому что вы не можете вычислить размер элемента, если он фактически не помещен в DOM (который имеет полный контекст, с помощью которого можно определить фактическую ширину визуализации). Помимо использования этого для таких вещей, как относительное позиционирование мыши, мне также нужно это для правильной установки атрибутов ширины для элементов SVG в компоненте.

Кроме того, при изменении размера окна, как мне сообщить об изменении размера от одного компонента к другому во время установки? Мы выполняем всю нашу стороннюю визуализацию SVG в shouldComponentUpdate, но вы не можете устанавливать состояние или свойства для себя или других дочерних компонентов в этом методе.

Есть ли стандартный способ решения этой проблемы с помощью React?

Стив Холлаш
источник
1
кстати, вы уверены, что shouldComponentUpdateэто лучшее место для рендеринга SVG? Похоже, вы хотите, componentWillReceivePropsа componentWillUpdateесли нет render.
Энди
1
Это может быть или не то, что вы ищете, но для этого есть отличная библиотека: github.com/bvaughn/react-virtualized Взгляните на компонент AutoSizer. Он автоматически управляет шириной и / или высотой, поэтому вам не нужно.
Maggie
@Maggie также проверьте github.com/souporserious/react-measure , это отдельная библиотека для этой цели, и она не будет помещать другие неиспользуемые вещи в ваш клиентский пакет.
Энди
эй, я ответил на аналогичный вопрос здесь. Это как-то другой подход, и он позволяет вам решать, что отображать в зависимости от вашего типа экрана (мобильный, планшет, настольный компьютер)
Calin ortan
@Maggie Я могу ошибаться в этом, но я думаю, что Auto Sizer всегда пытается заполнить своего родителя, а не определять размер, который ребенок принял, чтобы соответствовать его содержимому. Оба они полезны в немного разных ситуациях
Энди

Ответы:

65

Наиболее практичным решением является использование библиотеки, например, реакции-меры .

Обновление : теперь есть настраиваемый хук для обнаружения изменения размера (который я лично не пробовал): response-resize-aware . Будучи кастомным хуком, он выглядит более удобным в использовании, чем react-measure.

import * as React from 'react'
import Measure from 'react-measure'

const MeasuredComp = () => (
  <Measure bounds>
    {({ measureRef, contentRect: { bounds: { width }} }) => (
      <div ref={measureRef}>My width is {width}</div>
    )}
  </Measure>
)

Чтобы сообщить об изменениях размера между компонентами, вы можете передать onResizeобратный вызов и где-то сохранить значения, которые он получает (стандартный способ обмена состоянием в наши дни - использовать Redux ):

import * as React from 'react'
import Measure from 'react-measure'
import { useSelector, useDispatch } from 'react-redux'
import { setMyCompWidth } from './actions' // some action that stores width in somewhere in redux state

export default function MyComp(props) {
  const width = useSelector(state => state.myCompWidth) 
  const dispatch = useDispatch()
  const handleResize = React.useCallback(
    (({ contentRect })) => dispatch(setMyCompWidth(contentRect.bounds.width)),
    [dispatch]
  )

  return (
    <Measure bounds onResize={handleResize}>
      {({ measureRef }) => (
        <div ref={measureRef}>MyComp width is {width}</div>
      )}
    </Measure>
  )
}

Как кататься самостоятельно, если вы действительно предпочитаете:

Создайте компонент-оболочку, который обрабатывает получение значений из модели DOM и прослушивание событий изменения размера окна (или обнаружение изменения размера компонента, которое используется react-measure). Вы указываете ему, какие свойства нужно получить из DOM, и предоставляете функцию рендеринга, которая принимает эти свойства как дочерние.

То, что вы визуализируете, необходимо смонтировать, прежде чем можно будет прочитать свойства DOM; когда эти реквизиты недоступны во время первоначального рендеринга, вы можете захотеть использовать их, style={{visibility: 'hidden'}}чтобы пользователь не увидел их до того, как он получит макет, вычисленный с помощью JS.

// @flow

import React, {Component} from 'react';
import shallowEqual from 'shallowequal';
import throttle from 'lodash.throttle';

type DefaultProps = {
  component: ReactClass<any>,
};

type Props = {
  domProps?: Array<string>,
  computedStyleProps?: Array<string>,
  children: (state: State) => ?React.Element<any>,
  component: ReactClass<any>,
};

type State = {
  remeasure: () => void,
  computedStyle?: Object,
  [domProp: string]: any,
};

export default class Responsive extends Component<DefaultProps,Props,State> {
  static defaultProps = {
    component: 'div',
  };

  remeasure: () => void = throttle(() => {
    const {root} = this;
    if (!root) return;
    const {domProps, computedStyleProps} = this.props;
    const nextState: $Shape<State> = {};
    if (domProps) domProps.forEach(prop => nextState[prop] = root[prop]);
    if (computedStyleProps) {
      nextState.computedStyle = {};
      const computedStyle = getComputedStyle(root);
      computedStyleProps.forEach(prop => 
        nextState.computedStyle[prop] = computedStyle[prop]
      );
    }
    this.setState(nextState);
  }, 500);
  // put remeasure in state just so that it gets passed to child 
  // function along with computedStyle and domProps
  state: State = {remeasure: this.remeasure};
  root: ?Object;

  componentDidMount() {
    this.remeasure();
    this.remeasure.flush();
    window.addEventListener('resize', this.remeasure);
  }
  componentWillReceiveProps(nextProps: Props) {
    if (!shallowEqual(this.props.domProps, nextProps.domProps) || 
        !shallowEqual(this.props.computedStyleProps, nextProps.computedStyleProps)) {
      this.remeasure();
    }
  }
  componentWillUnmount() {
    this.remeasure.cancel();
    window.removeEventListener('resize', this.remeasure);
  }
  render(): ?React.Element<any> {
    const {props: {children, component: Comp}, state} = this;
    return <Comp ref={c => this.root = c} children={children(state)}/>;
  }
}

При этом реагировать на изменение ширины очень просто:

function renderColumns(numColumns: number): React.Element<any> {
  ...
}
const responsiveView = (
  <Responsive domProps={['offsetWidth']}>
    {({offsetWidth}: {offsetWidth: number}): ?React.Element<any> => {
      if (!offsetWidth) return null;
      const numColumns = Math.max(1, Math.floor(offsetWidth / 200));
      return renderColumns(numColumns);
    }}
  </Responsive>
);
Энди
источник
Один вопрос об этом подходе, который я еще не исследовал, заключается в том, мешает ли он SSR. Я еще не уверен, как лучше всего разобраться с этим делом.
Энди
отличное объяснение, спасибо за такую ​​тщательность :) re: SSR, здесь обсуждается isMounted(): facebook.github.io/react/blog/2015/12/16/…
ptim
1
@memeLab Я только что добавил код для красивого компонента-оболочки, который забирает большую часть стандартного кода из реакции на изменения DOM, взгляните :)
Энди
1
@Philll_t да, было бы неплохо, если бы DOM упростил это. Но поверьте мне, использование этой библиотеки избавит вас от неприятностей, даже если это не самый простой способ измерения.
Энди
1
@Philll_t еще одна вещь, о которой заботятся библиотеки, - это использование ResizeObserver(или полифил) для немедленного получения обновлений размера вашего кода.
Энди
43

Я думаю, что вам нужен метод жизненного цикла componentDidMount. Элементы уже размещены в DOM, и вы можете получить информацию о них из компонента refs.

Например:

var Container = React.createComponent({

  componentDidMount: function () {
    // if using React < 0.14, use this.refs.svg.getDOMNode().offsetWidth
    var width = this.refs.svg.offsetWidth;
  },

  render: function () {
    <svg ref="svg" />
  }

});
диван
источник
1
Будьте осторожны, offsetWidthэтого сейчас нет в Firefox.
Christopher Chiche
@ChristopherChiche Я не верю, что это правда. Какая у вас версия? По крайней мере, это работает для меня, и документация MDN, кажется, предполагает, что это можно предположить: developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model/…
couchand
1
Хорошо, это неудобно. В любом случае мой пример, вероятно, был неудачным, поскольку вы svgвсе равно должны явно указать размер элемента. AFAIK для всего, что вы ищете, на динамический размер вы, вероятно, можете положиться offsetWidth.
couchand
3
Для тех, кто приезжает сюда, используя React 0.14 или выше, .getDOMNode () больше не нужен: facebook.github.io/react/blog/2015/10/07/…
Хамунд
2
Хотя этот метод может показаться самым простым (и больше всего похожим на jQuery) способом доступа к элементу, Facebook теперь говорит: «Мы не рекомендуем его, потому что у строковых ссылок есть некоторые проблемы, они считаются устаревшими и, вероятно, будут удалены в будущем. выпусков. Если вы в настоящее время используете this.refs.textInput для доступа к ссылкам, мы рекомендуем вместо этого шаблон обратного вызова ". Вы должны использовать функцию обратного вызова вместо строки ref. Информация здесь
ahaurat
22

В качестве альтернативы решению couchand вы можете использовать findDOMNode

var Container = React.createComponent({

  componentDidMount: function () {
    var width = React.findDOMNode(this).offsetWidth;
  },

  render: function () {
    <svg />
  }
});
Лукаш Мадон
источник
10
Чтобы уточнить: в React <= 0.12.x используйте component.getDOMNode (), в React> = 0.13.x используйте React.findDOMNode ()
pxwise
2
@pxwise Aaaaa, а теперь для ссылок на элементы DOM вам даже не нужно использовать какую-либо функцию с React 0.14 :)
Энди
5
@pxwise, кажется, ReactDOM.findDOMNode()сейчас?
ivarni
1
@MichaelScheper Это правда, в моем коде есть какой-то ES7. В моем обновленном ответе react-measureдемонстрация (я думаю) чистый ES6. Трудно начинать, конечно ... Я прошел через то же безумие за последние полтора года :)
Энди
2
@MichaelScheper, кстати, вы можете найти здесь полезные советы: github.com/gaearon/react-makes-you-sad
Энди
6

Вы можете использовать библиотеку I, которую я написал, которая отслеживает размер ваших компонентов и передает их вам.

Например:

import SizeMe from 'react-sizeme';

class MySVG extends Component {
  render() {
    // A size prop is passed into your component by my library.
    const { width, height } = this.props.size;

    return (
     <svg width="100" height="100">
        <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
     </svg>
    );
  }
} 

// Wrap your component export with my library.
export default SizeMe()(MySVG);   

Демо: https://react-sizeme-example-esbefmsitg.now.sh/

Github: https://github.com/ctrlplusb/react-sizeme

Он использует оптимизированный алгоритм на основе прокрутки / объектов, который я позаимствовал у людей, гораздо более умных, чем я. :)

ctrlplusb
источник
Хорошо, спасибо, что поделились. Я собирался создать репозиторий для своего собственного решения для DimensionProvidingHoC. Но теперь я собираюсь попробовать это.
larrydahooster
Буду признателен за любые отзывы, положительные или отрицательные :-)
ctrlplusb
Спасибо тебе за это. <Recharts /> требует, чтобы вы явно указали ширину и высоту, так что это было очень полезно
JP