Search code examples
reactjscomponentsstatelessrefs

How do you get the height of a stateless functional component that's a child?


I'm trying to get the height of a stateless child component, so that I'm able to use its height within a parent Class, but I am getting the following error: Invariant Violation: Stateless function components cannot have refs.

Simplified code

Parent class

class App extends React.Component {
  componentDidMount() {
    console.log(this.header);
  }
  render() {
    return (
      <Child ref={component => this.header = component} />
    )
  }
}

Child

const Child = () => (
  <header ref="header">
    Some text  
  </header>
)

Is there a way to do this?

Here is a link to a Codepen with the error.

Update:

Actual code/context

So I've currently got a Header component, which looks like so:

export const Header = ({dateDetailsButton, title, logo, backButton, signUpButton, inboxButton, header}) => (
    <header header className="flex flex-row tc pa3 bb b--light-gray relative min-h-35 max-h-35">
      {signUpButton ? <Link to="/edit-profile-contact" href="#" className="flex z-2"><AddUser /></Link> : null }
      {backButton ? <BackButton className="flex z-2" /> : null }
      <h1 className={logo ? "w-100 tc dark-gray lh-solid f4 fw5 tk-reklame-script lh-solid ma0" : "w-100 tc dark-gray lh-solid f4 fw4 absolute left-0 right-0 top-0 bottom-0 maa h1-5 z-0"}>{title}</h1>
      {dateDetailsButton ? <Link to="/date-details" className="absolute link dark-gray right-1 top-0 bottom-0 maa h1-5 z-2">Details</Link> : null }
      {inboxButton ? <Link to="/inbox" href="#" className="flex mla z-2"><SpeechBubble /></Link> : null}
    </header>
)

In some instances, I want to add logic to this Header to animate it (for example, on the homepage when the user scrolls, I am animating the Header to become fixed when they scroll past a certain point - a sticky header, if you will).

The way I've done this before was to just have a separate Class for a Header with particular functionality (as described above) and one without. But in order to keep my code DRY I have separated out the Header to be its own functional stateless component, with the view to wrap it in Class that gives it the sticky header functionality.

Here's the Class for that:

export default class FeedHeader extends React.Component {

    constructor(props) {
        super(props);
        this.handleScroll = this.handleScroll.bind(this);
        this.state = {
            scrolledPastHeader: null,
            from: 0,
            to: 1,
        }
    }

    componentDidMount() {
        window.addEventListener('scroll', this.handleScroll);
        this.setState({navHeight: this.navHeight.offsetHeight});
    }

    componentWillUnmount() {
        window.removeEventListener('scroll', this.handleScroll);
    }

    handleScroll(e) {
        const { navHeight, scrolledPastHeader } = this.state;
        const wy = document.body.scrollTop;
        if (wy < navHeight) this.setState({scrolledPastHeader: null})
        else if (wy > navHeight && wy < 100) {
            this.setState({scrolledPastHeader: false})
        }
        else this.setState({scrolledPastHeader: true})
    }

    getMotionProps() {
        const { scrolledPastHeader } = this.state;

        return scrolledPastHeader === false ?
        {
            style: {
                value: this.state.from
            }
        }
        : {
            style: {
                value: spring(this.state.to)
            }
        }
    }

    render() {
        const { brand, blurb } = this.props;
        const { scrolledPastHeader } = this.state;
        return (
            <Motion {...this.getMotionProps()}>
                {({value}) => {
                    return (
                        <Header ref="child" title={this.props.title} backButton={false} signUpButton inboxButton logo/>
                    );
                }}
            </Motion>
        )
    }
}

So this is the context for this particular problem - I hope that makes things a little more clearer.

P.s sorry for the lack of context, I imagined the answer would be more straight forward than what it seems!


Solution

  • Ok, so first of all, thank you to everyone for their replies - it's really appreciated!

    I began implementing the react-waypoints as recommended by Win, although it became apparent I would need to modify my code a bit more to make it work, so I figured I'd have one last search to try and find an alternative solution using refs.

    The answer I was looking for was actually quite simple, and came from the following Github issue thread by mnpenner.

    The solution

    Because React doesn't allow refs on stateless functional component, instead you can wrap the functional component inside a wrapper while giving the wrapper its own ref, like so:

      render() {
        return (
          <div ref={el => {this.childWrap = el;}}>
            <Child />
          </div>
        )
      }
    

    You can then access the height of the component by targeting the wrapper:

      componentDidMount() {
            if(this.childWrap && this.childWrap.firstChild) {
            let childHeight = this.childWrap.offsetHeight;
                    console.log(childHeight);
        }
      }
    

    While this might not be the most elegant solution, it certainly solves my issue for the time-being.

    Here is a Codepen for reference.