React: How to get size(width/height) of element using ref?

Created on 25 Jun 2018  路  20Comments  路  Source: facebook/react

Do you want to request a feature or report a bug?
No

What is the current behavior?
Trying to get element height using ref, but always getting 0.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:

What is the expected behavior?
Getting the element height

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
React: 16.4.1, MAC OS, Yandex Browser

There is my code:

_element = React.createRef();

componentDidMount() {
console.log(this._element.current.clientHeight) -> it's 0
}
...
render() {
        const { children } = this.props;

        return (
            <div
                ref={this._element}
                className={'some classname'}>
                <div className={'header'}>
                    {some content}
                </div>
                {children()}
            </div>
        );
    }

Most helpful comment

I've seen similarly weird, but not identical behavior when trying to calculate element dimensions in componentDidMount. I've recently seen that when displaying a list of items that should be filling the entire viewable space -- roughly 360px, using clientWidth, if i call it during componentDidMount`, it will record something like 180px, and it records this value for a collection of items.

Seeing this on react 16.8.6.

componentDidMount() {
  this.setState({width: this.ref.current.parentElement.clientWidth}) // should be 360, reported as 180
}

However, if I introduce a setTimeout(), it works correctly:

componentDidMount() {
  setTimeout(() => {
    this.setState({width: this.ref.current.parentElement.clientWidth}) // 360
  }, 1)
}

All 20 comments

The ref value is the native DOM node, so if it's returning 0 it may be hidden, collapsed or there is a browser bug. I can't think of any place that React could be buggy here since the object is the native browser object and React doesn't do any styling or layout things that would cause it to be zero height (or report zero height)

I don't think it's a react problem, I think you should check your content and css as well. BTW: children is not a function.

The Element.clientHeight read-only property is zero for elements with no CSS or inline layout boxes, otherwise it's the inner height of an element in pixels, including padding but not the horizontal scrollbar height, border, or margin.

-- MDN

Try height or getBoundingclientRect.

Also not sure why current in this._element.current.clientHeight.

@stereobooster, when using React.createRef(), once the ref is attached, the dom element will be accessible from the ref.current attribute. https://reactjs.org/docs/forwarding-refs.html

In my case, it was a parent component that hidden this component. That's why it has 0 size - it rendered but hidden (display: none;) in parent

If the component needs to be initially hidden, the css rule visibility: hidden can be used instead, so that the component's size will be computed even if it's not visible.

I'm having exact same problem and element is ordinary img element without any negative display or visibility rules. Component is plain, no re-rendering (componentDidUpdate is not called, only componentDidMount). I want to set sibling's height to be the same when component mounts. But yeah, it's zero.
EDIT: it's parent same thing, no rules to hide, nothing is hidden, all good old HTML elements with backgrounds and borders.

The same thing here! I'm trying to get the height and width of an element so I can resize images to fit this element properly. The return is always zero

you need to use getBoundingClientRect().(whatever your looking for here).

So for height in the case, console.log(this._element.current.getBoundingClientRect().height). This is a fantastic function to use for DOM manipulation without using a third party library.

Check out the api https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect

For anybody reporting that the size is 0 please include the environment. The jsdom (which is used in most test environments) does not have a layout engine which means any size will return 0.

I've seen similarly weird, but not identical behavior when trying to calculate element dimensions in componentDidMount. I've recently seen that when displaying a list of items that should be filling the entire viewable space -- roughly 360px, using clientWidth, if i call it during componentDidMount`, it will record something like 180px, and it records this value for a collection of items.

Seeing this on react 16.8.6.

componentDidMount() {
  this.setState({width: this.ref.current.parentElement.clientWidth}) // should be 360, reported as 180
}

However, if I introduce a setTimeout(), it works correctly:

componentDidMount() {
  setTimeout(() => {
    this.setState({width: this.ref.current.parentElement.clientWidth}) // 360
  }, 1)
}

I鈥檓 guessing the reason you get the wrong data is because if you set state in componentdidmount, it actually calls render twice before the DOM gets updated.

When you call setTineout or setInterval, those actually get pushed out to the webapi to do the heavy lifting of setstate so it doesn鈥檛 call two renders before the DOM updates.

Hello. How I can get ref height and width in this case ?

import React, { Component } from 'react';

import './testMenu.css';

class testMenu extends Component {

    constructor(props) {
        super(props);

        this.state = {
            menu: [
                {
                    title: '袩褍薪泻褌 1',
                    active: true
                },
                {
                    title: '袩褍薪泻褌 2',
                    active: false
                },
                {
                    title: '袩褍薪泻褌 3',
                    active: false
                }
            ]
        };

        this._menuItems = [];
    }

    handlerClickItem = (indexClick) => {
        let menuItems = this.state.menu.slice();

        menuItems.map((item, indexItem) => {
            if (indexItem == indexClick) {
                item.active = true;
            } else {
                item.active = false;
            }
        });

        this.setState({
            menu: menuItems
        })
    };

    getStyleMenuBlock = () => {
        let menuItems = this.state.menu.slice(),
            ativeIndex = 0;

        menuItems.map((item, index) => {
            if (item.active) {
                ativeIndex = index;
            }
        });

        return {
            height: ? (this._menuItems[activeIndex].?)
            width: ? (this._menuItems[activeIndex].?)
        }
    };

    render() {
        return (
            <div className='test-block'>
                <ul className='test-block__menu'>
                    <React.Fragment>
                        {
                            this.state.menu.map((item, index) =>
                                <li
                                    className={`test-block__item ${ item.active ? 'ugsk-test-menu__item_active' : '' }`}
                                    onClick={ () => this.handlerClickItem(index) }
                                    ref={ref => { this._menuItems[index] = ref; return true; }}
                                >{ item.title }</li>
                            )
                        }
                        <li className='test-block__block' style={ this.getStyleMenuBlock() }></li>
                    </React.Fragment>
                </ul>
            </div>
        )
    }
}

I fix this used ReactDOM

import React, { Component } from 'react';

import ReactDOM from 'react-dom';

import './testMenu.css';

class testMenu extends Component {

    constructor(props) {
        super(props);

        this.state = {
            menu: [
                {
                    title: '袩褍薪泻褌 1',
                    active: true
                },
                {
                    title: '袩褍薪泻褌 2',
                    active: false
                },
                {
                    title: '袩褍薪泻褌 3',
                    active: false
                }
            ]
        };

        this._menuItems = [];
        this._menuBlock = null;
    }

    shouldComponentUpdate(nextProps, nextState, nextContext) {
        if (this.state.menu != nextState.menu) {
            return true;
        }

        return false;
    }

    componentDidMount() {
        this.getStyleMenuBlock();
    }

    handlerClickItem = (indexClick) => {
        let menuItems = this.state.menu.slice();

        menuItems.map((item, indexItem) => {
            if (indexItem == indexClick) {
                item.active = true;
            } else {
                item.active = false;
            }
        });

        this.setState({
            menu: menuItems
        })
    };

    getStyleMenuBlock = () => {
        let menuItems = this.state.menu.slice(),
            activeIndex = 0;

        menuItems.map((item, index) => {
            if (item.active) {
                activeIndex = index;
            }
        });


        this._menuBlock.style.width = ReactDOM.findDOMNode(this._menuItems[activeIndex]).getBoundingClientRect().width + 'px';
        this._menuBlock.style.height = ReactDOM.findDOMNode(this._menuItems[activeIndex]).getBoundingClientRect().height + 'px';
    };

    render() {
        return (
            <div className='test-menu'>
                <ul className='test-menu__menu'>
                    <React.Fragment>
                        {
                            this.state.menu.map((item, index) =>
                                <li
                                    className={`test-menu__item ${ item.active ? 'test-menu__item_active' : '' }`}
                                    onClick={ () => this.handlerClickItem(index) }
                                    ref={ref => { this._menuItems[index] = ref; return true; }}
                                >{ item.title }</li>
                            )
                        }
                        <li
                            className='test-menu__block'
                            ref={ref => { this._menuBlock = ref }}
                        />
                    </React.Fragment>
                </ul>
            </div>
        )
    }
}
export default testMenu;

I've seen similarly weird, but not identical behavior when trying to calculate element dimensions in componentDidMount. I've recently seen that when displaying a list of items that should be filling the entire viewable space -- roughly 360px, using clientWidth, if i call it during componentDidMount`, it will record something like 180px, and it records this value for a collection of items.

Seeing this on react 16.8.6.

componentDidMount() {
  this.setState({width: this.ref.current.parentElement.clientWidth}) // should be 360, reported as 180
}

However, if I introduce a setTimeout(), it works correctly:

componentDidMount() {
  setTimeout(() => {
    this.setState({width: this.ref.current.parentElement.clientWidth}) // 360
  }, 1)
}

Damn.... Why this works?

Experienced the same problem recently and here the solution I found:

const ref = useRef();

useEffect(() => {
    document.onreadystatechange = () => {
      console.log(ref.current.clientHeight);
    };
  }, []);

I believe using class component will work the same way:

componentDidMount() {
  document.onreadystatechange = () => {
    console.log(this.ref.current.clientHeight);
  };
}

Experienced the same problem recently and here the solution I found:

const ref = useRef();

useEffect(() => {
    document.onreadystatechange = () => {
      console.log(ref.current.clientHeight);
    };
  }, []);

I believe using class component will work the same way:

componentDidMount() {
  document.onreadystatechange = () => {
    console.log(this.ref.current.clientHeight);
  };
}

This worked for me

Experienced the same problem recently and here the solution I found:

const ref = useRef();

useEffect(() => {
    document.onreadystatechange = () => {
      console.log(ref.current.clientHeight);
    };
  }, []);

I believe using class component will work the same way:

componentDidMount() {
  document.onreadystatechange = () => {
    console.log(this.ref.current.clientHeight);
  };
}

this does not work. Onreadystatechange does not perform in component did mount

setTimeout works for me too. But why?

The element isn't actually rendered to the DOM when the component is mounted and therefore you will get 0 in return. I'd recommend something along these lines

useEffect(() => {
    setStateRowDataTimeWidth(rowDataRef.current.clientWidth);
  }, [rowDataTimeRef.current?.clientWidth]);

This will initially show 0 for a split second then register the real width. I'd then use the state held value instead of the one on the ref to ensure you are getting the latest copy and that rerenders the component.

Was this page helpful?
0 / 5 - 0 ratings