0

I want to do something only in component is in viewport. It almost working, but I have a one problem with which I can not manage.

class Hello extends React.Component {

  componentDidMount() {
    console.log(this.isInViewPort()) // false for second element - last in viewport
    if (this.isInViewPort()) {
    console.log("I'm in viewport!");
    }
    window.addEventListener('scroll', this.handleScroll);
  }

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

  isInViewPort = () => {
    const element = this.refs[this.props.id];
    const rect = element.getBoundingClientRect();
    const windowHeight = window.innerHeight || document.documentElement.clientHeight;
    const result = rect.top <= windowHeight && rect.top + rect.height >= 0;
    return result;
  }

  handleScroll = () => {
    if (this.isInViewPort()) {
        console.log("I'm in viewport!");
    }
  }

  render() {
    return (
        <div className="example" ref={this.props.id} onClick={this.handleScroll} /> // onClick added for tests
    );
  }
}


class App extends React.Component {
  render() {
    return (
      <Hello id={1} />
      <Hello id={2} />
      <Hello id={3} />
    );
  }
}


.example {
  width: 400px;
  height: 400px;
  background-color: red;
}

When I loading the page, two elements are in viewport. First one - 100% in viewport and second one - maybe 15-20% in viewport.

For this example I have only one console.log, so this.isInViewPort() is true only for first element. It's wrong, cause two of them are visible.

When I touch the event scroll, Component immediately visible for method and make console log. When I do not touch a scroll, but click on second component (this is added for tests) method return true and i get console.log.

I want to make situation when every visible elements returned true by method.

It works when I do this:

componentDidMount() {
    setTimeout(() => {
      if (this.isInViewPort()) {
        console.log("I'm in viewport!");
    }, 0);
}

But for 100% this is not a good solution. It also do not work when i put this code after refactor to componentDidUpdate().

It's something with react lifecycles? I do not doing anything witch state, that's why it is not understandable to me.

sosick
  • 624
  • 3
  • 17
  • 35
  • what version of react are you working with? – Sagiv b.g Apr 16 '19 at 13:59
  • My React version is 16.8.3 – sosick Apr 16 '19 at 14:01
  • @sosick after opening the devtool, use `Ctrl + Shift + D` to change the placement of devtool. And refresh the browser, you can see different result. For horizontal/ bottom position: `true`, `false` and `false` and for vertical/ right position: `true`, `true` and `false`. So it depends on how you are viewing. – Sahil Raj Thapa Apr 16 '19 at 14:54

3 Answers3

1

Works ok

class Hello extends React.Component {
  componentDidMount() {
    this.isInViewPort()
    window.addEventListener('scroll', this.handleScroll);
  }

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

  isInViewPort = () => {
    const element = this.refs[this.props.id];
    const rect = element.getBoundingClientRect();
    const windowHeight = window.innerHeight || document.documentElement.clientHeight;
    const result = rect.top <= windowHeight && rect.top + rect.height >= 0;
    this.props.onLog(result ? `${this.props.id} in view port` : `${this.props.id} outside`)
    return result;
  }


  handleScroll = () => {
    this.isInViewPort()
  }

  render() {
    return (<div className="example" ref={this.props.id} onClick={this.handleScroll} />
    );
  }
}
class App extends React.Component {
  render() {
    return <div>
      <Hello id={1} onLog={console.info} />
      <Hello id={2} onLog={console.info} />
      <Hello id={3} onLog={console.info} />
     </div>;
  }
}

ReactDOM.render(<App /> ,
  document.getElementById('container')
);
.example {
  width: 400px;
  height: 400px;
  background-color: red;
}

.example:first-child {
  background-color: pink;
}

.example:last-child {
  background-color: blue;
}
.as-console-wrapper {
  height: 62px!important;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="container" />
Medet Tleukabiluly
  • 11,662
  • 3
  • 34
  • 69
1

I believe your issue is probably related to where you test your project and how you deliver your files etc. I created a fiddle to test it:

.example {
  width: 400px;
  height: 90vh;
  background-color: red;
  border: 1px solid black;
}

https://jsfiddle.net/284Ldvkc/

I only changed height from 400px to 90vh, so I'd make sure that always 1 is in view and 2 is in view with 10%.

Doğancan Arabacı
  • 3,934
  • 2
  • 17
  • 25
  • You're right. The spinner below my components made that second component is not visible when componentDidMount. Thanks. – sosick Apr 16 '19 at 16:00
1

In StackOverflow, you can find the best way to check if a "DOM element is visible in the current viewport".

I just added a:

isElementInViewport = el => {
    var rect = el.getBoundingClientRect();

    return (
      rect.top >= 0 &&
      rect.left >= 0 &&
      rect.bottom <=
        (window.innerHeight ||
          document.documentElement.clientHeight) /*or $(window).height() */ &&
      rect.right <=
        (window.innerWidth ||
          document.documentElement.clientWidth) /*or $(window).width() */
    );
  };

and call it from:

isInViewPort = () => {
    const element = this.refs[this.props.id];
    console.log("isInViewPort:" + this.props.id);
    return this.isElementInViewport(element);
  };

You can test this on codesandbox.io

r.pedrosa
  • 709
  • 5
  • 12