Reputation: 157
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
Reputation: 3996
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:
querySelector
to target a child of an element that you don't have direct access to.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 )true
by default.Open Code in Stackblitz
PS: The custom hook is maintained and updated here for more use cases.
Upvotes: 0
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
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
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
Reputation: 1542
You have at least three options how to do that:
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
Track ‘y’ scroll position explicitly with https://react-fns.netlify.com/docs/en/api.html#scroll
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