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

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

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

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

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

Ответ 1

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

Примечание: этот код не работает с [email protected]^2.0.0 по мере изменения API. Перейдите по ссылке выше, чтобы увидеть новый API.

import Measure from 'react-measure'

const MeasuredComp = () => (
  <Measure>
    {({width}) => <div>My width is {width}</div>}
  </Measure>
)

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

import Measure from 'react-measure'
import connect from 'react-redux'
import {setMyCompWidth} from './actions' // some action that stores width in somewhere in redux state

function select(state) {
  return {
    currentWidth: ... // get width from somewhere in the state
  }
}

const MyComp = connect(select)(({dispatch, currentWidth}) => (
  <Measure onMeasure={({width}) => dispatch(setMyCompWidth(width))}>
    <div>MyComp width is {currentWidth}</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>
);

Ответ 2

Я думаю, что метод жизненного цикла, который вы ищете, 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" />
  }

});

Ответ 3

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

var Container = React.createComponent({

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

  render: function () {
    <svg />
  }
});

Ответ 4

Вы можете использовать библиотеку 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

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