React Lazy Load with Intersection Observer API

Simple demo to learn Intersection Observer API

by cadenzah

HTML

<html>
<body>

</body>
</html>

CSS

.card-grid {
  display: flex;
  flex-direction: column;
  align-items: center;
  
  width: 100%;
  height: 350px;
  border: 1px solid black;
  
  overflow: auto;
}

.card {
  display: flex;
  align-items: center;
  
  background-color: skyblue;
  width: 50%;
  height: 100px;
  min-height: 100px;
  margin: 20px;
    
}

JavaScript

const { useEffect, useState } = React;

import { getProducts } from '../../api';
import Product from './product';

const Products = () => {
  const [products, setProducts] = useState([]);

  // 요 훅이 가장 먼저 실행된다
  const [observer, setElements, entries] = useIO({
    threshold: 0.25,
    rootMargin: '30px',
    root: null,
  });

  // 가장 처음, categoryId를 기반으로 상품 "전체" 목록 로드 (데이터)
  useEffect(() => {
    const fetchData = async () => {
      const result = await getProducts(categoryId);
      setProducts(result.data.payload);
    };

    fetchData();
  }, [categoryId]);

  // 'lazy' 클래스를 가진 이미지 요소들을 참조 초기화 (뷰)
  // 각 뷰 요소는 존재하며, "이미지 파일 로드만 안 이루어진 상태"
  useEffect(() => {
    if (products.length) {
      let img = Array.from(document.getElementsByClassName('lazy'));
      setElements(img);
    }
  }, [products, setElements]);

  // 현재 뷰포트 내에 존재하는 요소에 대하여 이미지 로드 처리
  useEffect(() => {
    entries.forEach(entry => {
      // 최초 실행시, 바로 뷰포트에 보이는 요소들은 바로 로딩 시작
      if (entry.isIntersecting) {
        let lazyImage = entry.target;
        lazyImage.src = lazyImage.dataset.src;
        lazyImage.classList.remove("lazy");
        // 원래 최초에는 모든 요소가 observe 되어있는 상태
        observer.unobserve(lazyImage); // 관련 attr 갱신이 완료되면 해당 요소 unobserve
      }
    });
  }, [entries, observer]);

  return (<div></div>);
}

const useIO = (options) => {
  // 뷰 요소들
  const [elements, setElements] = useState([]);
  // observer가 감시하고 있는 요소 대상
  // - IntersectionObserverElement로, DOMElement와 다름
  const [entries, setEntries] = useState([]);

  const observer = useRef(null);
  const { root, rootMargin, threshold } = options || {};

  useEffect(() => {
    // 최초에는 elements가 []이므로 실행 안댐
    // 두번째 이후에서는 elements에는 "모든" 이미지 요소가 들어있다
    if (elements.length) {
      console.log('OBSERVER CONNECTING');

      // (1) 새 observer를 선언하고
      observer.current = new IntersectionObserver((ioEntries) => {
        setEntries(ioEntries);
      }, {
        threshold, root, rootMargin,
      });

      // (2)...