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

前端 未结 4 1963

I have a complex web page using React components, and am trying to convert the page from a static layout to a more responsive, resizable layout. However, I keep running into

4条回答
  •  死守一世寂寞
    2020-12-22 18:09

    The most practical solution is to use a library for this like react-measure.

    Update: there is now a custom hook for resize detection (which I have not tried personally): react-resize-aware. Being a custom hook, it looks more convenient to use than react-measure.

    import * as React from 'react'
    import Measure from 'react-measure'
    
    const MeasuredComp = () => (
      
        {({ measureRef, contentRect: { bounds: { width }} }) => (
          
    My width is {width}
    )}
    )

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

    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,
    };
    
    type Props = {
      domProps?: Array,
      computedStyleProps?: Array,
      children: (state: State) => ?React.Element,
      component: ReactClass,
    };
    
    type State = {
      remeasure: () => void,
      computedStyle?: Object,
      [domProp: string]: any,
    };
    
    export default class Responsive extends Component {
      static defaultProps = {
        component: 'div',
      };
    
      remeasure: () => void = throttle(() => {
        const {root} = this;
        if (!root) return;
        const {domProps, computedStyleProps} = this.props;
        const nextState: $Shape = {};
        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 {
        const {props: {children, component: Comp}, state} = this;
        return  this.root = c} children={children(state)}/>;
      }
    }
    

    With this, responding to width changes is very simple:

    function renderColumns(numColumns: number): React.Element {
      ...
    }
    const responsiveView = (
      
        {({offsetWidth}: {offsetWidth: number}): ?React.Element => {
          if (!offsetWidth) return null;
          const numColumns = Math.max(1, Math.floor(offsetWidth / 200));
          return renderColumns(numColumns);
        }}
      
    );
    

提交回复
热议问题