I have updated my implementation of useIntersectionObserver hook, please refer to this post.
In this article, I'll write a React hook and a React component that will help you achieve lazy loading in ReactJS.
What is Intersection Observer API?
Basically, Intersection Observer will monitor elements and check if they're intersect with the viewport of an document or, most of time, the browser viewport.
For more information, please refer to the MDN docs.
Create the React hook
First, let's start with an empty hook like this:
export function useIntersectionObserver(){
}
Then we can add a state that will tell us if the component is intersecting and return that state:
export function useIntersectionObserver(){
const [isIntersecting, setIsIntersecting] = useState(false);
return isIntersecting;
}
Now, we need a ref that can hold the observer:
export function useIntersectionObserver(){
const [isIntersecting, setIsIntersecting] = useState(false);
const observer = useRef<null | IntersectionObserver>(null);
return isIntersecting;
}
Since we need a target element for the observer, let's add a parameter and state to the hook function:
export function useIntersectionObserver(ref: MutableRefObject<Element | null>){
const [element, setElement] = useState<Element | null>(null);
const [isIntersecting, setIsIntersecting] = useState(false);
const observer = useRef<null | IntersectionObserver>(null);
useEffect(() => {
setElement(ref.current);
}, [ref]);
return isIntersecting;
}
Now, we can create a observer to observe the Element:
export function useIntersectionObserver(ref: MutableRefObject<Element | null>){
const [element, setElement] = useState<Element | null>(null);
const [isIntersecting, setIsIntersecting] = useState(false);
const observer = useRef<null | IntersectionObserver>(null);
useEffect(() => {
setElement(ref.current);
}, [ref]);
useEffect(() => {
if (!element) return;
const ob = observer.current = new IntersectionObserver(([entry]) => {
const isElementIntersecting = entry.isIntersecting;
setIsIntersecting(isElementIntersecting);
})
ob.observe(element);
}, [element])
return isIntersecting;
}
And don't forget to disconnect the observer once the component is unmounted or the target element is changed.
export function useIntersectionObserver(ref: MutableRefObject<Element | null>){
const [element, setElement] = useState<Element | null>(null);
const [isIntersecting, setIsIntersecting] = useState(false);
const observer = useRef<null | IntersectionObserver>(null);
const cleanOb = () => {
if (observer.current) {
observer.current.disconnect()
}
}
useEffect(() => {
setElement(ref.current);
}, [ref]);
useEffect(() => {
if (!element) return;
cleanOb();
const ob = observer.current = new IntersectionObserver(([entry]) => {
const isElementIntersecting = entry.isIntersecting;
setIsIntersecting(isElementIntersecting);
})
ob.observe(element);
return () => {
cleanOb()
}
}, [element])
return isIntersecting;
}
Now, we want to be able to configure the observer, so let's add the options to the hook function as a parameter:
export function useIntersectionObserver(ref: MutableRefObject<Element | null>, options: IntersectionObserverInit = {}){
const [element, setElement] = useState<Element | null>(null);
const [isIntersecting, setIsIntersecting] = useState(false);
const observer = useRef<null | IntersectionObserver>(null);
const cleanOb = () => {
if (observer.current) {
observer.current.disconnect()
}
}
useEffect(() => {
setElement(ref.current);
}, [ref]);
useEffect(() => {
if (!element) return;
cleanOb();
const ob = observer.current = new IntersectionObserver(([entry]) => {
const isElementIntersecting = entry.isIntersecting;
setIsIntersecting(isElementIntersecting);
}, { ...options })
ob.observe(element);
return () => {
cleanOb()
}
}, [element, options ])
return isIntersecting;
}
For more information about the options, please refer to the MDN docs.
Last, since we usually don't want to remove the content we've rendered, let's add a parameter that allow us to choice if we want the observer to be disconnected after the target element is intersected.
export function useIntersectionObserver(ref: MutableRefObject<Element | null>, options: IntersectionObserverInit = {}, forward: boolean = true) {
const [element, setElement] = useState<Element | null>(null);
const [isIntersecting, setIsIntersecting] = useState(false);
const observer = useRef<null | IntersectionObserver>(null);
const cleanOb = () => {
if (observer.current) {
observer.current.disconnect()
}
}
useEffect(() => {
setElement(ref.current);
}, [ref]);
useEffect(() => {
if (!element) return;
cleanOb()
const ob = observer.current = new IntersectionObserver(([entry]) => {
const isElementIntersecting = entry.isIntersecting;
if (!forward) {
setIsIntersecting(isElementIntersecting)
} else if (forward && !isIntersecting && isElementIntersecting) {
setIsIntersecting(isElementIntersecting);
cleanOb()
};
}, { ...options })
ob.observe(element);
return () => {
cleanOb()
}
}, [element, options ])
return isIntersecting;
}
Create a Lazy Loading Component
Once we have the hook we need, it's very simple to create a lazy loading componentwith it:
interface LazyLoadProps {
tag?: ComponentType | keyof JSX.IntrinsicElements
children: ReactNode
style?: CSSProperties
className?: string
root?: Element | Document | null
threshold?: number | number[]
rootMargin?: string
forward?: boolean
}
export function LazyLoad(props: LazyLoadProps) {
const { tag = 'div', children, style, className } = props;
const Tag: any = tag;
const ref = useRef<Element>(null)
const isIntersecting = useIntersectionObserver(ref, {
root: props.root ?? null,
threshold: props.threshold ?? 0,
rootMargin: props.rootMargin
}, props.forward);
return (
<Tag
ref={ref}
style={style}
className={className}
children={isIntersecting ? children : null}
/>
)
}
And, here we go.
Thank you for reading this article. Please let me know if there is any issue I made.
The hook and the Lazyload component are included in my npm package ax-react-lib.
Top comments (4)
Thanks, very helpfull.
Eslint can warning about cleanOb return in useEffect.
As workaround you can return undefined in first line.
refercence:
stackoverflow.com/questions/676589...
You probably should use
useLayoutEffect
to avoid setting element to the state.Hey, you can't put a ref as a dependency of a useEffect
epicreact.dev/why-you-shouldnt-put...
You're right. It was just mean to get rid of lint warning about deps at the time. Thanks for pointing out.