7

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!

a7dc
  • 3,323
  • 7
  • 32
  • 50
  • Remove `ref="header"` from child's component `
    `
    – Fawaz Aug 05 '17 at 19:55
  • This can help https://stackoverflow.com/questions/35153599/reactjs-get-height-of-an-element – Fawaz Aug 05 '17 at 19:56
  • 2
    whats wrong with `npm i react-dimensions` and then `@Dimensions` as HOC will provide you with `containerWidth` and `containerHeight` props. – Dimitar Christoff Aug 05 '17 at 20:03
  • I've updated the OP to give it context. `react-dimensions` doesn't look like it will work because I need to access the height of a child component which then needs to be accessed via the parent. – a7dc Aug 05 '17 at 20:24
  • 1
    so create a new component that uses react-dimensions as an outer element to host the one that cares. expose the props to the child and the parent, recompose the child before rendering. the reason why it should not be a home brew solution is that you'd have to cater for all sorts - scroll, resize events, get the correct document element etc. better to make a tested solution work. – Dimitar Christoff Aug 05 '17 at 21:06
  • Thanks Dimitar, I'll try this tomorrow with fresh eyes! Much appreciated – a7dc Aug 05 '17 at 21:42

2 Answers2

8

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.

a7dc
  • 3,323
  • 7
  • 32
  • 50
  • Ug, there should be a better way. There must be a way to get a reference to the functional component's dom element. – mtyson Jan 10 '20 at 21:23
1

Here's a solution using React-waypoint. You can check it out here: https://codesandbox.io/s/qp3ly687

I've added a debounce to the onEnter and onLeave so that it doesn't update the state like crazy, this is so we can optimise the performance when scrolling and it won't lock up the application. Again, it's a very rough idea of what you can do and there's plenty of options to improve the implementation.

=== Previous

This is one way that you can get around the issue by keeping the stateless component. Since the context wasn't explained further, this should give you an opportunity to see how you can grab the height of a child component that sits within a stateless component.

class App extends React.Component {
  componentDidMount() {
    console.log(this.child.offsetHeight);
  }
  render() {
    return (
      <div>
        <Wrapper>
          <div 
            ref={child => this.child = child}
            style={{height: 500}}>
            Some text!!
          </div>
         </Wrapper>
       </div>
    )
  }
}

const Wrapper = ({children}) => {
  return (
    <div>
      <header>Header</header>
      <div>{children}</div>
      <footer>Footer</footer>
    </div>
  )
}

ReactDOM.render(
  <App />,
  document.getElementById("main")
);
Win
  • 5,498
  • 2
  • 15
  • 20
  • Thanks, I think I'll update the OP to give a better understanding of context. I appreciate your answer. – a7dc Aug 05 '17 at 20:14
  • I've updated the OP with context. Figured this would be a lot more simple than it appears to be lol. – a7dc Aug 05 '17 at 20:23
  • 1
    @A7DC tricky indeed, have you considered something like React-Waypoints? Instead of trying to calculating the height that has been scrolled past to activate sticky mode? I think it's one of those situations where using refs could be avoided. – Win Aug 05 '17 at 20:26
  • Forgive my ignorance, but I think you'd still need to know the height of the header to animate it when you wanted to (without hardcoding it, because I guess the height won't change regardless of screen size in this instance). Ignoring the code in the following example, this is a pretty accurate representation of the interaction: https://codepen.io/A7DC/pen/WjEWGj Notice how it becomes fixed exactly once the window has scrolled past the Header? – a7dc Aug 05 '17 at 20:33
  • 1
    @A7DC It's very easy to get confused about waypoints but it will allow you to do the same thing. It works by placing an element after the header component and then once you scroll past the element ( when it reaches the top ) you can trigger an event which can be a state change that applies a class (fixed for example) to your header. – Win Aug 05 '17 at 20:37
  • Ah, legendary. I'll check that out further then. Thank you! – a7dc Aug 05 '17 at 20:39
  • 1
    @A7DC I'll try and find a solution for you by digging around as I did this awhile back. – Win Aug 05 '17 at 20:43
  • Thanks, Win. I'll report back if I figure it out too! It may be a case of just changing the child component to a Class and using refs and coming back to this at a later date – a7dc Aug 05 '17 at 20:48
  • 1
    @A7DC I've just updated my answer to show a rough idea of how you can do it with react-waypoint :) too. It's a bit niffy but I'm sure there's lots of improvement space. – Win Aug 05 '17 at 21:54
  • Hey Win, thanks for your help - you're awesome! I found an approach elsewhere which allows me to not change too much code and just get the height by wrapping the Child component inside a div and getting the refs there. I'm not sure if this is the best solution but its one that works. Thank you again for your time and help! – a7dc Aug 06 '17 at 14:34