Как я могу реагировать на ширину элемента 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

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


Хорошо, спасибо, что поделились. Я собирался создать репозиторий для своего собственного решения для DimensionProvidingHoC. Но теперь я собираюсь попробовать это.
— larrydahooster

Буду признателен за любые отзывы, положительные или отрицательные :-)
— ctrlplusb

Спасибо тебе за это. <Recharts /> требует, чтобы вы явно указали ширину и высоту, так что это было очень полезно
— JP
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.