Easy Lazy Loading with React & Intersection Observer API
Tags: react, typescript, hooks, javascript
Published: 2021-04-18 06:26:40

> I have updated my implementation of useIntersectionObserver hook, please refer to this [post](https://dev.to/anxiny/custom-react-hook-useintersection-with-intersection-observer-4l4e).


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](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).

## Create the React hook

First, let's start with an empty hook like this:

```typescript
export function useIntersectionObserver(){
}
```

Then we can add a state that will tell us if the component is intersecting and return that state:

```typescript
export function useIntersectionObserver(){
  const [isIntersecting, setIsIntersecting] = useState(false);
  return isIntersecting;
}
```

Now, we need a ref that can hold the observer:

```typescript
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:

```typescript
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:

```typescript
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.

```typescript
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:

```typescript
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](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).

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.

```typescript
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:
```typescript

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](https://www.npmjs.com/package/ax-react-lib).