reboottime / WebDevelopment

Some notes, thoughts and articles aggregated here about UI/UX and web development.
6 stars 0 forks source link

Intersection Observer API and its application #163

Open reboottime opened 1 year ago

reboottime commented 1 year ago

Overview

This article talks about the application of Intersection Observer API and its compatibility consideration.

The basic API

const options = {
  root: document.querySelector("#scrollArea"),
  rootMargin: "0px",
  threshold: 1.0,
};

let observer = new IntersectionObserver(callback, options);

let callback = (entries, observer) => {
  entries.forEach((entry) => {
    // Each entry describes an intersection change for one observed
    // target element:
    //   entry.boundingClientRect
    //   entry.intersectionRatio
    //   entry.intersectionRect
    //   entry.isIntersecting
    //   entry.rootBounds
    //   entry.target
    //   entry.time
  });
};
reboottime commented 1 year ago

Intersection Observer API Applications


Application on Image Lazy loading

import { useState, useEffect, useRef } from 'react';

export default function useLoadImg() {
    const [state, setState] = useState<'loading' | 'loaded' | 'error'>();
    const loadFuncRef = useRef<CallableFunction>(emptyFunc);

    useEffect(() => {
        loadFuncRef.current = async (src: string) => {
            setState('loading');

            try {
                await loadImg(src);
                setState('loaded');
            } catch (e) {
                setState('error');
            }
        }
    }, []);

    return {
        state,
        loadImg: loadFuncRef.current
    };
}

function loadImg(src: string) {
    return new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = () => {
            resolve(void);
        };
        img.onerror = () => {
            reject();
        }
    });
};

function emptyFunc (...args:any[]) {
}

const PLACEHOLDER_IMG_URL = '';

const LazyImage = ({ src, alt, className }) => {
  const imgRef = useRef(null);
  const { state: imgState, loadImg } = useLoadImg();

  useEffect(() => {
    const viewportHeight = window.innerHeight;

    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.boundingClientRect.top < 1.5 * viewportHeight) {
          loadImg(src);
        }
      });
    });
    const imgElem = imgRef.current;

    if (imgElem) {
      observer.observe(imgElem);
    }

    return () => {
      if (imgElem) {
        observer.unobserve(imgElem);
      }
    };
  }, [loadImg, src]);

  const isVisible = imgState === 'loaded';

  return (
    <img
      alt={alt}
      ref={imgRef}
      src={isVisible ? src : PLACEHOLDER_IMG_URL}
    />
  );
};
reboottime commented 1 year ago

Application on Infinite loading

Use case: Implementing "infinite scrolling" websites, where more and more content is loaded and rendered as you scroll, so that the user doesn't have to flip through pages.


import React, { useEffect, useRef, useState } from 'react';

const InfiniteScrollComponent: React.FC = () => {
  const observer = useRef<IntersectionObserver | null>(null);
  const [page, setPage] = useState(1);
  const [isLoading, setIsLoading] = useState(false);
  const containerRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    // Initialize Intersection Observer when component mounts
    observer.current = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting) {
        // When the observed element is in view, load more content
        setIsLoading(true);
        setPage(prevPage => prevPage + 1);
      }
    });

    // Start observing the container element
    if (containerRef.current) {
      observer.current.observe(containerRef.current);
    }

    return () => {
      // Clean up observer when component unmounts
      if (observer.current) {
        observer.current.disconnect();
      }
    };
  }, []);

  useEffect(() => {
    // Simulate loading more content (you would fetch data from an API here)
    if (isLoading) {
      // Example fetch using window.fetch
      fetch(`https://api.example.com/data?page=${page}`)
        .then(response => response.json())
        .then(data => {
          // Handle the loaded data
          console.log('Loaded data:', data);
          setIsLoading(false);
        })
        .catch(error => {
          console.error('Error fetching data:', error);
          setIsLoading(false);
        });
    }
  }, [isLoading, page]);

  return (
    <div>
      <div>
        {/* Render your content here */}
        {/* ... */}
      </div>
      <div ref={containerRef}>
        {/* This is the element that will be observed */}
      </div>
    </div>
  );
};

export default InfiniteScrollComponent;
reboottime commented 1 year ago

Application on adding css animation to html element that just jumped in user's view


import React, { useRef, useEffect } from 'react';
import './AnimatedComponent.css'; // Create this file for your CSS animation

const AnimatedComponent = () => {
  const animatedRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          entry.target.classList.add('animate'); // Add the CSS class for animation
          observer.unobserve(entry.target); // Stop observing once animation is applied
        }
      },
      { threshold: 0.1 } // Adjust threshold as needed
    );

    if (animatedRef.current) {
      observer.observe(animatedRef.current);
    }

    return () => {
      if (animatedRef.current) {
        observer.unobserve(animatedRef.current);
      }
    };
  }, []);

  return <div className="animated-element" ref={animatedRef}></div>;
};

export default AnimatedComponent;
reboottime commented 1 year ago

References

  1. MDN Documentation: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
  2. The IntersectionObserverEntry: https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry
  3. Can I use reference: https://caniuse.com/intersectionobserver