47

I'm working on an agenda/calendar app with a variable time range. To display a line for the current time and show blocks for appointments that have been made, I need to calculate how many pixels correspond with one minute inside the given time range.

So for example: If the agenda starts at 7 o'clock in the morning and ends at 5 o'clock in the afternoon, the total range is 10 hours. Let's say that the body of the calendar has a height of 1000 pixels. That means that every hour stands for 100 pixels and every minute for 1,66 pixels.

If the current time is 3 o'clock in the afternoon. We are 480 minutes from the start of the agenda. That means that the line to show the current time should be at 796,8 pixels (480 * 1,66) from the top of the calendar body.

No problems with the calculations but with getting the height of the agenda body. I was thinking to use a React Ref to get the height but I'm getting an error: ref.current is null

Below some code:

class Calendar extends Component {
    calendarBodyRef = React.createRef();

    displayCurrentTimeLine = () => {
        const bodyHeight = this.calendarBodyRef.current.clientHeight; // current is null
    }

    render() {
        return (
            <table>
                <thead>{this.displayHeader()}</thead>
                <tbody ref={this.calendarBodyRef}>
                    {this.displayBody()}
                    {this.displayCurrentTimeLine()}
                </tbody>
            </table>
        );
    }
}
Thore
  • 1,918
  • 2
  • 25
  • 50
  • Read this: https://medium.com/welldone-software/usecallback-might-be-what-you-meant-by-useref-useeffect-773bc0278ae – aderchox Feb 21 '22 at 09:05

4 Answers4

31

So the thing about refs is that they aren't guaranteed to be set on first render. You can be sure they are set during and after componentDidMount so you have two ways going forward.

You could use the callback style ref and set state based on that. E.g. instead of passing your ref as the prop, you can pass in a reference to a function like this.handleRef and it would do some logic in there:

  handleRef = r => {
    this.setState({ bodyHeight: r.clientHeight})
    this.calendarBodyRef.current = r;
  };

Or, you could keep your current set up but you would have to move your clientHeight bit to a lifecycle function like:

  componentDidMount() {
    this.setState({ bodyHeight: this.calendarBodyRef.current.clientHeight });
  }

Ultimately, you can't immediately read the current value of a ref like that, you would have to check it after the render and then read your bodyHeight from state.

Chrillewoodz
  • 27,055
  • 21
  • 92
  • 175
Tom Finney
  • 2,670
  • 18
  • 12
  • I'm just going to use an id and document.getElementById instead. I love React but this is a React design issue. It popped up inexplicably when I switched a component from class to function. What's the harm of this workaround? Using getElementById is working fine so far. – Dan Cancro Jul 05 '20 at 19:20
  • 3
    I mean you can totally do that if you want but design like that can become extremely fragile because you can potentially run into issues like having the multiple instances of the component rendered sharing the same id and it can make refactoring or generalising components a bit annoying. – Tom Finney Jul 06 '20 at 12:00
  • Thanks. I'm going to stick with it. There are tradeoffs for sure. The approach I'm taking is less convoluted and there isn't any chance of there being multiple instances of this component. So if that's the only foreseeable hazard, then I'm happy with my decision. – Dan Cancro Jul 06 '20 at 15:49
  • @DanCancro That isn't a React design issue. The element doesn't even exist before the component mounts, and it couldn't possibly, so you won't be able to query it that way. It makes sense to have to use the ref to the element in a function called after the element is actually on the page. – Grant Gryczan Aug 12 '21 at 22:33
6

You could use a ref callback function. In this case, you wouldn't need to use "React-createRef()".

<tbody ref={this.calendarBodyRef}>
...
calendarBodyRef = (e) => {
console.log(e)
}

You will get the DOM Element back and therefore don't need to use "current".

3

If you are using react-redux and wrapped your component in connect function then you need to pass fourth argument i.e. forwardRef like this.

connect(mapStateToProps, mapDispatchToProps, null, {forwardRef: true})

I hope it will be helpful.

Uddesh Jain
  • 1,064
  • 2
  • 15
  • 16
0

If avoiding storage of the calculated body height in component state is preferred, then another approach would be to introduce an second ref (ie elementDisplayHeightRef) like so:

class Calendar extends React.Component {

    /* Create a ref for the body */
    calendarBodyRef = React.createRef();

    /* Create a ref for element where height will be displayed */
    elementDisplayHeightRef = React.createRef();

    displayCurrentTimeLine = () => {

        /* Calculate body height from ref */
        const bodyHeight = this.calendarBodyRef.current.clientHeight;    

        /* Update display */
        this.elementDisplayHeightRef.current.innerText = `bodyHeight:${bodyHeight}`
    }

    render() {
        return (
            <table>
                <thead></thead>
                <tbody ref={this.calendarBodyRef}>
                    <td><td>Some row</td></td>
                    {/* Bind display ref */ }
                    <tr><td ref={this.elementDisplayHeightRef}></td></tr>
                </tbody>
            </table>
        );
    }

    /* Add did mount life cycle hook, and trigger display of body height */
    componentDidMount() {

      this.displayCurrentTimeLine()
    }
}

This approach calls displayCurrentTimeLine() during the componentDidMount() life cycle hook (which itself is called after the first render()) to ensure that both refs are fully initialised, before the component logic interacts with them in displayCurrentTimeLine().

Hope that helps!

Dacre Denny
  • 29,664
  • 5
  • 45
  • 65