如何响应 React 中自动调整大小的 DOM 元素的宽度?

IT技术 javascript reactjs responsive
2021-05-08 23:40:47

我有一个使用 React 组件的复杂网页,我正在尝试将页面从静态布局转换为响应速度更快、可调整大小的布局。但是,我一直遇到 React 的限制,并且想知道是否有处理这些问题的标准模式。在我的特定情况下,我有一个组件呈现为带有 display:table-cell 和 width:auto 的 div。

不幸的是,我无法查询组件的宽度,因为您无法计算元素的大小,除非它实际放置在 DOM 中(它具有推断实际渲染宽度的完整上下文)。除了将它用于诸如相对鼠标定位之类的事情之外,我还需要它来正确设置组件内 SVG 元素的宽度属性。

此外,当窗口调整大小时,如何在设置过程中将大小更改从一个组件传递到另一个组件?我们在 shouldComponentUpdate 中进行了所有的 3rd-party SVG 渲染,但是您不能在该方法中为自己或其他子组件设置状态或属性。

是否有使用 React 处理此问题的标准方法?

4个回答

最实用的解决方案是为此使用一个库,例如react-measure

更新:现在有一个用于调整大小检测的自定义钩子(我没有亲自尝试过):react-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 获取哪些 props 并提供一个渲染函数,将这些 props 作为子级。

在 DOM props 可以被读取之前,你渲染的东西必须被挂载;当这些props在初始渲染期间不可用时,您可能想要使用,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>
);

我认为您正在寻找的生命周期方法是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" />
  }

});

替代沙发解决方案,您可以使用 findDOMNode

var Container = React.createComponent({

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

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

您可以使用我编写的库来监控您的组件渲染大小并将其传递给您。

例如:

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-esbefmitg.now.sh/

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

它使用了一种优化的基于滚动/对象的算法,这是我从比我聪明得多的人那里借来的。:)