Shatha
Shatha

Reputation: 157

ReactJS how to render a component only when scroll down and reach it on the page?

I have a react component Data which includes several charts components; BarChart LineChart ...etc.

When Data component starts rendering, it takes a while till receiving the data required for each chart from APIs, then it starts to respond and render all the charts components.

What I need is, to start rendering each chart only when I scroll down and reach it on the page.

Is there any way could help me achieving this??

Upvotes: 14

Views: 35490

Answers (5)

Anjan Talatam
Anjan Talatam

Reputation: 3996

Custom hook: useIsScrollComplete

Usecase: Show a component when the scroll of one component is complete.

I came up with a custom hook that does the job.

Note: The below snippet is not runnable. Check this Stackblitz for runnable code.

function useIsScrollComplete<TElement extends HTMLElement | null>({
  ref,
  querySelector,
  markAsComplete = true,
}: IUseIsScrollComplete<TElement>) {
  const [isScrollComplete, setIsScrollComplete] = useState(false);

  const onScroll: EventListener = useCallback(({ target }) => {
    const { scrollHeight, clientHeight, scrollTop } = target as Element;

    if (Math.abs(scrollHeight - clientHeight - scrollTop) < THRESHOLD) {
      setIsScrollComplete(true);
    } else {
      setIsScrollComplete(false);
    }
  }, []);

  useEffect(() => {
    const element = ref.current;
    const targetElement = querySelector
      ? element?.querySelector(querySelector)
      : element;

    if (targetElement) {
      const { scrollHeight, clientHeight } = targetElement;

      if (scrollHeight === clientHeight) {
        // set scroll is complete if there is no scroll
        setIsScrollComplete(true);
      }

      targetElement.addEventListener("scroll", onScroll);

      if (isScrollComplete && markAsComplete) {
        targetElement.removeEventListener("scroll", onScroll);
      }

      return () => {
        targetElement.removeEventListener("scroll", onScroll);
      };
    }
  }, [isScrollComplete, markAsComplete, onScroll, querySelector, ref]);

  return { isScrollComplete };
}

Usage:

  const divRef = useRef<HTMLDivElement | null>(null);
  const { isScrollComplete } = useIsScrollComplete({ ref: divRef });

  return (
    <div>
      <div ref={divRef}>
           <p>Scrollable Content</p>
      </div>

      {isScrollComplete && (
        <p>Scroll is Complete ✅</p>
      )}
    </div>
  );

Other use-cases:

  1. You can use querySelector to target a child of an element that you don't have direct access to.
  2. markAsComplete prop -> specifies whether to mark the scroll as complete. Defaults to true. If set to false, the scroll is observed even after the scroll is complete. i.e if you move back from the bottom to the top, isScrollComplete will be false. ( Ex: When you want to show pagination of the table only when the scroll is at the bottom of the table and should hide when the scroll is anywhere else )
  3. If the container does not have scroll, the value is set to true by default.

Open Code in Stackblitz

PS: The custom hook is maintained and updated here for more use cases.

Upvotes: 0

hamdi islam
hamdi islam

Reputation: 1521

I have tried many libraries but couldn't find something that best suited my needs so i wrote a custom hook for that, I hope it helps

import { useState, useEffect } from "react";

const OPTIONS = {
  root: null,
  rootMargin: "0px 0px 0px 0px",
  threshold: 0,
};

const useIsVisible = (elementRef) => {
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    if (elementRef.current) {
      const observer = new IntersectionObserver((entries, observer) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            setIsVisible(true);
            observer.unobserve(elementRef.current);
          }
        });
      }, OPTIONS);
      observer.observe(elementRef.current);
    }
  }, [elementRef]);

  return isVisible;
};

export default useIsVisible;

and then you can use the hook as follows :

import React, { useRef } from "react";
import useVisible from "../../hooks/useIsVisible";

function Deals() {
  const elemRef = useRef();
  const isVisible = useVisible(elemRef);
  return (
    <div ref={elemRef}>hello {isVisible && console.log("visible")}</div>
)}

Upvotes: 14

Odis
Odis

Reputation: 85

I think the easiest way to do this in React is using react-intersection-observer.

Example:

import { useInView } from 'react-intersection-observer';

const Component = () => {
  const { ref, inView, entry } = useInView({
    /* Optional options */
    threshold: 0,
  });

  useEffect(()=>{
      //do something here when inView is true
  }, [inView])

  return (
    <div ref={ref}>
      <h2>{`Header inside viewport ${inView}.`}</h2>
     </div>
  );
};

I also reccommend using triggerOnce: true in the options object so the effect only happens the first time the user scrolls to it.

Upvotes: 3

Witold Tkaczyk
Witold Tkaczyk

Reputation: 713

you can check window scroll position and if the scroll position is near your div - show it. To do that you can use simple react render conditions.

import React, {Component} from 'react';
import PropTypes from 'prop-types';

class MyComponent extends Component {
constructor(props){
    super(props);

    this.state = {
        elementToScroll1: false,
        elementToScroll2: false,
    }

    this.firstElement = React.createRef();
    this.secondElement = React.createRef();
}
componentDidMount() {
    window.addEventListener('scroll', this.handleScroll);
}

componentWillUnmount() {
    window.removeEventListener('scroll', this.handleScroll);
}
handleScroll(e){
    //check if scroll position is near to your elements and set state {elementToScroll1: true}
    //check if scroll position is under to your elements and set state {elementToScroll1: false}
}
render() {
    return (
        <div>
            <div ref={this.firstElement} className={`elementToScroll1`}>
                {this.state.elementToScroll1 && <div>First element</div>}
            </div>
            <div ref={this.secondElement} className={`elementToScroll2`}>
                {this.state.elementToScroll2 && <div>Second element</div>}
            </div>
        </div>
    );
}
}

MyComponent.propTypes = {};

export default MyComponent;

this may help you, it's just a quick solution. It will generate you some rerender actions, so be aware.

Upvotes: 1

Dima Vishnyakov
Dima Vishnyakov

Reputation: 1542

You have at least three options how to do that:

  1. Track if component is in viewport (visible to user). And then render it. You can use this HOC https://github.com/roderickhsiao/react-in-viewport

  2. Track ‘y’ scroll position explicitly with https://react-fns.netlify.com/docs/en/api.html#scroll

  3. Write your own HOC using Intersection Observer API https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API

To render component you may need another HOC, which will return Chart component or ‘null’ based on props it receives.

Upvotes: 14

Related Questions