88

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 limitations with React, and am wondering if there's a standard pattern for handling these issues. In my specific case, I have a component that renders as a div with display:table-cell and width:auto.

Unfortunately, I cannot query the width of my component, because you can't compute the size of an element unless it's actually placed in the DOM (which has the full context with which to deduce the actual rendered width). Besides using this for things like relative mouse positioning, I also need this to properly set width attributes on SVG elements within the component.

In addition, when the window resizes, how do I communicate size changes from one component to another during setup? We're doing all of our 3rd-party SVG rendering in shouldComponentUpdate, but you cannot set state or properties on yourself or other child components within that method.

Is there a standard way of dealing with this problem using React?

Andy
  • 7,885
  • 5
  • 55
  • 61
Steve Hollasch
  • 2,011
  • 1
  • 20
  • 18
  • 1
    by the way, are you sure `shouldComponentUpdate` is the best place to render SVG? It sounds like what you want is `componentWillReceiveProps` or `componentWillUpdate` if not `render`. – Andy Nov 21 '15 at 01:52
  • 1
    This may or not be what you are looking for, but there is an excellent library for this: https://github.com/bvaughn/react-virtualized Have a look at the AutoSizer component. It automatically manages width and/or height, so you don't have to. – Maggie Jun 23 '16 at 21:37
  • @Maggie check out https://github.com/souporserious/react-measure also, it's a standalone library for this purpose, and wouldn't put other unused stuff into your client bundle. – Andy Oct 25 '16 at 15:10
  • hey I answered a similar question [here](https://stackoverflow.com/a/45269365/7974438) It's somehow a different approach and it let's you decide what to render depending on your scren type(mobile, tablet, desktop) – Calin ortan Jul 26 '17 at 09:13
  • @Maggie I could be wrong about this, but I think Auto Sizer always tries to fill its parent, rather than detect the size is child has taken to fit it's content. Both are useful in slightly different situations – Andy Oct 31 '17 at 14:30

4 Answers4

66

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 = () => (
  <Measure bounds>
    {({ measureRef, contentRect: { bounds: { width }} }) => (
      <div ref={measureRef}>My width is {width}</div>
    )}
  </Measure>
)

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 (
    <Measure bounds onResize={handleResize}>
      {({ measureRef }) => (
        <div ref={measureRef}>MyComp width is {width}</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>
);
Andy
  • 7,885
  • 5
  • 55
  • 61
  • One question about this approach I haven't investigated yet is whether it interferes with SSR. I'm not yet sure what the best way to handle that case would be. – Andy Oct 28 '15 at 23:48
  • great explanation, thanks for being so thorough :) re: SSR, There's a discussion of `isMounted()` here: https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html – ptim Feb 29 '16 at 22:22
  • 1
    @memeLab I just added code for a nice wrapper component that takes most of the boilerplate out of responding to DOM changes, take a look :) – Andy Apr 19 '16 at 23:50
  • @ptim in any case I rewrote the code so that it doesn't need to check `isMounted()` – Andy Mar 03 '17 at 18:24
  • This seems like an overly complicated way to get the width of a component :/ – Philll_t Apr 15 '18 at 18:24
  • @Philll_t which does, `react-measure` or my from-scratch example? – Andy Apr 17 '18 at 02:35
  • @Andy , just the whole process in general. Your example is outstanding, my comment is more addressing the fact that we have to call in an entire library to get a decent measurement. – Philll_t Apr 17 '18 at 02:37
  • 1
    @Philll_t yes it would be nice if the DOM made this easier. But trust me, using this library will save you trouble, even though it's not the most basic way to get a measurement. – Andy Apr 17 '18 at 03:54
  • 1
    @Philll_t another thing that libraries take care of is using `ResizeObserver` (or a polyfill) to get size updates to your code immediately. – Andy Apr 18 '20 at 19:06
43

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

});
Tanner Semerad
  • 12,472
  • 12
  • 40
  • 49
couchand
  • 2,639
  • 1
  • 21
  • 27
  • 1
    Be careful that `offsetWidth` does not currently exist in Firefox. – Christopher Chiche Jul 24 '15 at 15:33
  • @ChristopherChiche I don't believe that's true. What version are you running? It works for me at least, and the MDN documentation seems to suggest that it can be assumed: https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model/Determining_the_dimensions_of_elements – couchand Aug 24 '15 at 13:59
  • I might be wrong but last time I checked, `offsetWidth` on a `svg` element returned undefined. – Christopher Chiche Aug 24 '15 at 14:41
  • 1
    Well I'll be, that's inconvenient. In any case my example was probably a poor one, since you must explicitly size an `svg` element anyway. AFAIK for anything you'd be looking for the dynamic size of you can probably rely on `offsetWidth`. – couchand Aug 24 '15 at 15:25
  • 3
    For anyone coming here using React 0.14 or above, the .getDOMNode() is no longer needed: https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html#dom-node-refs – Hamund Nov 11 '15 at 09:27
  • @couchand I don't think the OP was looking to *get* the width of an SVG element, just *set* it based upon something else, so you could revise your answer to use some other element. – Andy Nov 21 '15 at 01:54
  • note that you'll typically want to remeasure the size at least when the window resizes, as this would not be responsive at all. – Andy Apr 22 '16 at 18:20
  • 2
    While this method may feel like the easiest (and most like jQuery) way to access the element, Facebook now says "We advise against it because string refs have some issues, are considered legacy, and are likely to be removed in one of the future releases. If you're currently using this.refs.textInput to access refs, we recommend the callback pattern instead". You should use a callback function instead of a string ref. [Info Here](https://facebook.github.io/react/docs/refs-and-the-dom.html) – ahaurat Apr 12 '17 at 13:36
22

Alternatively to couchand solution you can use findDOMNode

var Container = React.createComponent({

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

  render: function () {
    <svg />
  }
});
Lukasz Madon
  • 14,664
  • 14
  • 64
  • 108
  • 10
    To clarify: in React <= 0.12.x use component.getDOMNode(), in React >= 0.13.x use React.findDOMNode() – pxwise May 13 '15 at 19:30
  • 2
    @pxwise Aaaaaand now for DOM element refs you don't even have to use either function with React 0.14 :) – Andy Oct 08 '15 at 23:13
  • 5
    @pxwise I believe it's `ReactDOM.findDOMNode()` now? – ivarni Feb 29 '16 at 10:56
  • @pxwise "Note that refs to custom (user-defined) components work exactly as before; only the built-in DOM components are affected by this change." – Lukasz Madon Feb 29 '16 at 12:05
  • I *so* wish I'd read this before spending so much time trying to get @Andy's answer to work in ES6. Three lines of code, instead of all that stuff that I'm sure is brilliant, but goes over my head, in my second week of React and ES6. ☺ – Michael Scheper Sep 16 '16 at 18:12
  • @MichaelScheper sure, but this doesn't actually respond to changes in the element's width, it only checks the size when it mounts. I'll simplify my answer by recommending a great library for this... – Andy Sep 16 '16 at 23:36
  • @Andy: Fair point, and recommending a library sounds good. I think some of your code is ES7, though? Which would be fair enough, but if it's possible to add a couple of words for people like me who are just starting out with React and ES6/ES7, it might help others too. – Michael Scheper Sep 16 '16 at 23:52
  • 1
    @MichaelScheper It's true, there is some ES7 in my code. In my updated answer the `react-measure` demonstration is (I think) pure ES6. It's tough getting started, for sure...I went through the same madness over the past year and a half :) – Andy Sep 16 '16 at 23:59
  • 2
    @MichaelScheper btw, you might find some useful guidance here: https://github.com/gaearon/react-makes-you-sad – Andy Sep 17 '16 at 00:02
6

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. :)

ctrlplusb
  • 12,847
  • 6
  • 55
  • 57