How can I respond to the width of an auto-sized DOM element in React?

好久不见. 提交于 2019-11-29 18:50:24

The most practical solution is to use react-measure.

Note: this code does not work with react-measure@^2.0.0 as the API has changed. Visit the above link to see the new API.

import Measure from 'react-measure'

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

To communicate size changes between components, you can pass an onMeasure callback and store the values it receives somewhere (the standard way of sharing state these days is to use 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>
))

How to roll your own if you really prefer to:

Create a wrapper component that handles getting values from the DOM and listening to window resize events (or component resize detection as used by react-measure). You tell it which props to get from the DOM and provide a render function taking those props as a child.

What you render has to get mounted before the DOM props can be read; when those props aren't available during the initial render, you might want to use style={{visibility: 'hidden'}} so that the user can't see it before it gets a JS-computed layout.

// @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)}/>;
  }
}

With this, responding to width changes is very simple:

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>
);
couchand

I think the lifecycle method you're looking for is componentDidMount. The elements have already been placed in the DOM and you can get information about them from the component's refs.

For instance:

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" />
  }

});

Alternatively to couchand solution you can use findDOMNode

var Container = React.createComponent({

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

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

You could use I library I wrote which monitors your components rendered size and passes it through to you.

For example:

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);   

Demo: https://react-sizeme-example-esbefmsitg.now.sh/

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

It uses an optimised scroll/object based algorithm that I borrowed from people much more clever than I am. :)

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!