37

I'm attempting to integrate or create a React version of https://github.com/kumailht/gridforms, to do so I need to normalize the height of the columns inside of the row. The original takes the height of the grid row and applies it to the children columns.

I had planned to get the height of the row and then map it to a property of the child, though from my attempts I'm thinking this might not be the ideal way or even possible?

Below is my current code.

GridRow = React.createClass({
  render(){
        const children = _.map(this.props.children, child => {
            child.props.height = // somehow get row component height
            return child
        })
    return (<div data-row-span={this.props.span} {...this.props}>
      {children}
    </div>)
  }
})

GridCol = React.createClass({
  render(){
    return (<div data-field-span={this.props.span} style={{height:this.props.height}} {...this.props}>
      {this.props.children}
    </div>)
  }
})

I tested setting the style this way and it will work, however getting the height isn't.

EDIT: Fiddle:

https://jsfiddle.net/4wm5bffn/2/

Patrick Cauley
  • 961
  • 2
  • 11
  • 20
  • Is it throwing an error or something, or just not working? Also, would you mind setting up a JSFiddle so I can better understand your problem? – Michael Parker Jul 27 '15 at 19:42
  • There doesn't seem to be documentation on getting the components height or other css when it renders and passing it to the children. I'll set up a jsfiddle. I think I found the correct documentation in getting the DOM node in the `componentDidMount()` method, and then using jQuery to get the CSS. I'm wondering if there is a jQuery free way of getting the components style. – Patrick Cauley Jul 27 '15 at 19:45
  • Edited with jsfiddle. – Patrick Cauley Jul 27 '15 at 19:53
  • 4
    You can use the getDOMNode method of the react component. You could then use it to access properties like any other javascript DOM element. https://facebook.github.io/react/docs/component-api.html#getdomnode – cwbutler Jul 27 '15 at 19:55
  • Are you sure that's the correct jsfiddle link? – Michael Parker Jul 27 '15 at 20:00
  • @MichaelParker woops. It's corrected now. – Patrick Cauley Jul 27 '15 at 20:02
  • In your fiddle, it looks like `GridRow` and `GridCol` are nearly identical, with the exception of a single `console.log()` in `GridCol`. Was this intended? – Michael Parker Jul 27 '15 at 20:06
  • @MichaelParker Yes, there is the `data-field-span` and `data-row-span` which correspond with the CSS. I think I could collapse them into a single component but didn't seem important. I'm updating with a new fiddle. https://jsfiddle.net/4wm5bffn/2/ – Patrick Cauley Jul 27 '15 at 20:10
  • I believe part of your problem is the way you are mapping over the child elements. Take a look at this: https://facebook.github.io/react/docs/top-level-api.html#react.children and this http://stackoverflow.com/questions/31658732/setting-a-prop-of-a-child-component-inside-a-parent-component-in-reactjs – cwbutler Jul 27 '15 at 20:20
  • @cwbutler this is an updated fiddle, https://jsfiddle.net/4wm5bffn/3/, however this requires getting the rows height for each column and using state for the style. Passing it in as a prop doesn't work as `componentDidMount` in the parent component doesn't call a rerender thus not passing it down. – Patrick Cauley Jul 27 '15 at 20:25
  • After looking over it, I honestly think you're overthinking this. Is there a reason why you can't just use CSS to set the `GridCol`'s height to 100%? Then as long as every instance of `GridCol` is a child of `GridRow`, it will have the same height as the row. – Michael Parker Jul 27 '15 at 20:55
  • Setting it to 100% didn't seem to work without setting a height on the `GridRow`. I probably am over thinking it. I did get something that worked so I'm mostly happy with it. I was going off of what was done in the original library and that used JS to set the height so maybe I was relying too much on the original authors thoughts. – Patrick Cauley Jul 29 '15 at 16:40

9 Answers9

19

A bit late with the answer but technically you can get element hight this way:

var node = ReactDOM.findDOMNode(this.refs[ref-name]);
if (node){
  var calculatedHeight = node.clientHeight;
}
anyab
  • 359
  • 5
  • 11
  • 14
    `ReactDOM` is no longer needed. If you use `ref={(input) => { this.myElement = input; }} />` then all you need is `this.myElement.clientHeight`. – Wylliam Judd Oct 19 '17 at 18:13
  • 1
    What if you want to get the height of a sibling or parent, say, a navbar, and your working inside another component. Then `this.refs` wont work – conor909 Nov 14 '17 at 14:14
19

According to current React docs, the preferred use of refs is to pass it a callback rather than a string to be accessed elsewhere in this.refs. So to get the height of a div (within a React.Component class):

componentDidMount() {
  this.setState({ elementHeight: this.divRef.clientHeight });
}

render() {
  return <div ref={element => this.divRef = element}></div>
}

Or it works this way, though I don't know if this is advisable since we set state in the render method.

getHeight(element) {
  if (element && !this.state.elementHeight) { // need to check that we haven't already set the height or we'll create an infinite render loop
    this.setState({ elementHeight: element.clientHeight });
  }
}

render() {
  return <div ref={this.getHeight}></div>;
}

Reference: https://facebook.github.io/react/docs/more-about-refs.html

Andy_D
  • 4,112
  • 27
  • 19
  • I'm fairly new to react however I think it might be due to the fact that he changes the state in componentDidMount and from render which to my knowledge is a no no, and also, he's returning a assignment with an arrow function – Touchpad Jan 04 '17 at 12:55
  • 2
    right from the docs: `ref={(input) => { this.textInput = input; }} />` – Scott Silvi Feb 09 '17 at 00:08
  • It's unnecessary to set state here, but otherwise it is worth mentioning that the new way of using refs is with a function. – Wylliam Judd Oct 19 '17 at 18:11
10

Don't know about anyone else but I always have to get it on the next tick to be sure of getting the correct height and width. Feels hacky but guessing it's to do with render cycle but I'll take it for now. onLayout may work better in certain use cases.

componentDidMount() {
  setTimeout(() => {
    let ref = this.refs.Container
    console.log(ref.clientHeight)
    console.log(ref.clientWidth)
  }, 1)
}
Brian F
  • 1,620
  • 1
  • 19
  • 23
9

Here is an example of using refs and clientWidth/clientHeight:

import React, { Component } from 'react';
import MyImageSrc from './../some-random-image.jpg'

class MyRandomImage extends Component {
  componentDidMount(){
    let { clientHeight, clientWidth } = this.refs.myImgContainer;
    console.log(clientHeight, clientWidth);
  }
  render() {
    return (
      <div ref="myImgContainer">
        <img src={MyImageSrc} alt="MyClickable" />
      </div>
    );
  }
}

export default MyRandomImage;

Note: this appears to work for width reliably, but not height. Will edit if I find a fix...

DeBraid
  • 8,751
  • 5
  • 32
  • 43
1

My personal opinion is to try and avoid using static and measured sizes like this if you can avoid it because it can complicate the application unnecessarily. But sometimes you cannot get around it. Your component will need to be mounted before you can get a size from it.

General approach:

  • Give the element a ref
  • When the element is rendered, grab the ref and call .clientHeight and/or .clientWidth
  • Put the values on the state or pass with props
  • Render the element that needs the size from the state variables

In your case you want to grab the size of a column you can do something like:

GridRow = React.createClass({
  render(){
        const children = _.map(this.props.children, child => {
            child.props.height = // somehow get row component height
            return child
        })

    return (<div data-row-span={this.props.span} {...this.props}>
      <GridCol onSizeChange={(size) => {

         //Set it to state or whatever
         console.log("sizeOfCol", size);
      }} />
    </div>)
  }
})

GridCol = React.createClass({

  componentDidMount(){

    //Set stizes to the local state
    this.setState({
       colH: this.col.clientHeight,
       colW: this.col.clientWidth
    }); 

    //Use a callback on the props to give parent the data
    this.props.onSizeChange({colH: this.col.clientHeight, colW: this.col.clientWidth})

  }

  render(){
    //Here you save a ref (col) on the class
    return (<div ref={(col) => {this.col = col}} data-field-span={this.props.span} style={{height:this.props.height}} {...this.props}>
      <.... >
    </div>)
  }
})
Victor Axelsson
  • 1,420
  • 1
  • 15
  • 32
1

According this answer sizes of a component can be turned out having zero width or height inside componentDidMount event handler. So I'm seeing some ways to solve it.

  1. Handle the event on top-level React component, and either recalculate the sizes there, or redraw the specific child component.

  2. Set the load event handler on the componentDidMount to handle loading the cells into the react component to recalculate the proper sizes:

    componentDidMount = () => {
       this.$carousel = $(this.carousel)
       window.addEventListener('load', this.componentLoaded)
    }
    

    Then in the componentLoaded method just do what you need to do.

Alex C
  • 516
  • 7
  • 15
Малъ Скрылевъ
  • 16,187
  • 5
  • 56
  • 69
1

A bit more late, but I have an approach which can be used without using the getElementById method. A class based component could be created and the sample code can be used.

constructor(props) {
  super(props);
  this.imageRef = React.createRef();
}

componentDidMount(){
  this.imageRef.current.addEventListener("load", this.setSpans);
}

setSpans = () => {
  //Here you get your image's height
  console.log(this.imageRef.current.clientHeight);
};

render() {
  const { description, urls } = this.props.image;
  return (
    <div>
     <img ref={this.imageRef} alt={description} src={urls.regular} />
    </div>
  );
}
Sunil Kumar
  • 388
  • 5
  • 15
0

Above solutions are good. I thought I'd add my own that helped me solve this issue + others discussed in this question.

Since as others have said a timeout function is unpredictable and inline css with javascript variable dependencies (ex. style={{height: `calc(100vh - ${this.props.navHeight}px)`}}) can alter the height of elements after the componentDidMount method, there must be an update after all of the elements and inline javascript-computed css is executed.

I wasn't able to find very good information on which elements accept the onLoad attribute in React, but I knew the img element did. So I simply loaded a hidden image element at the bottom of my react component. I used the onLoad to update the heights of referenced components elsewhere to yield the correct results. I hope this helps someone else.

_setsectionheights = () => {
    this.setState({
      sectionHeights: [
        this.first.clientHeight,
        this.second.clientHeight,
        this.third.clientHeight,
      ]
    });
}

render() {
    return (
        <>
            <section
            ref={ (elem) => { this.first = elem } }
            style={{height: `calc(100vh - ${this.props.navHeight}px)`}}
            >
                ...
            </section>
            ...
            <img style={{display: "none"}} src={..} onLoad={this._setsectionheights}/>
        </>
    );
}

For the sake of being thorough, the issue is that when the componentDidMount method is executed, it only considers external css (speculation here). Therefore, my section elements (which are set to min-height: 400px in external css) each returned 400 when referenced with the clientHeight value. The img simply updates the section heights in the state once everything before it has loaded.

-1

I'd rather do it in componentDidUpdate, but by making sure a condition is met to prevent an infinite loop:

  componentDidUpdate(prevProps, prevState) {
    const row = document.getElementById('yourId');
    const height = row.clientHeight;
    
    if (this.state.height !== height) {
      this.setState({ height });
    }
  }
Benoit
  • 1,922
  • 16
  • 25
  • 3
    You should almost never use `getElementById` in react. As with Python: an exception is not exceptional enough. – Berry M. Feb 08 '19 at 06:49
  • Completely agreed. But sometimes, you know, there is no better solution but the need is still there. It is better for the person that asks to get the range of possibility. This method does not comply with the philosophy of react, but sounds less hackish and unpredictible than a setTimeout for instance. – Benoit Feb 09 '19 at 09:55