react-intersection-observer

repository·main·Indexed 26 days ago

https://github.com/thebuilder/react-intersection-observer

A React implementation of the Intersection Observer API used to detect when elements enter or leave the viewport. Version 10.1.0 provides the useInView and useOnInView hooks, as well as the <InView /> component supporting Render Props and Plain Children patterns. It includes options for rootMargin, threshold, and triggerOnce, as well as test utilities for mocking IntersectionObserver in Jest and Vitest.

Tokens
7.1K
Snippets
17
Records
33
Agent score
88%

What's inside react-intersection-observer

  1. Configure Vitest for IntersectionObserver mocking

    main

    If you are using Vitest without globals, you must manually set up and reset the intersection mocking in your test files or a setup file.

    import { vi, beforeEach, afterEach } from "vitest";
    import {
      setupIntersectionMocking,
      resetIntersectionMocking,
    } from "react-intersection-observer/test-utils";
    
    beforeEach(() => {
      setupIntersectionMocking(vi.fn);
    });
    
    afterEach(() => {
      resetIntersectionMocking();
    });
    import { vi, beforeEach, afterEach } from "vitest";
    import {
      setupIntersectionMocking,
      resetIntersectionMocking,
    } from "react-intersection-observer/test-utils";
    
    beforeEach(() => {
      setupIntersectionMocking(vi.fn);
    });
    
    afterEach(() => {
      resetIntersectionMocking();
    });
  2. Configure fallback behavior for unsupported IntersectionObserver

    main

    If a client's browser does not support IntersectionObserver, the library throws an error by default. You can prevent this by setting a fallback inView value.

    Global Fallback

    Set a global fallback value (either true or false) using defaultFallbackInView in a setup file.

    import { defaultFallbackInView } from "react-intersection-observer";
    
    defaultFallbackInView(true);

    Local Fallback

    Override the global setting for a specific hook or component using the fallbackInView option.

    import React from "react";
    import { useInView } from "react-intersection-observer";
    
    const Component = () => {
      const { ref, inView } = useInView({
        fallbackInView: true,
      });
    
      return (
        <div ref={ref}>
          <h2 className={`Header inside viewport ${inView}.`}/>
        </div>
      );
    };
    import { defaultFallbackInView } from "react-intersection-observer";
    
    defaultFallbackInView(true); // or `false`
    
    // OR locally in a component:
    const { ref, inView } = useInView({
      fallbackInView: true,
    });
  3. Track impressions with useOnInView

    main

    To track when a user views a specific element (e.g., for analytics or marketing impressions), use the useOnInView hook. This hook accepts a callback function that is executed whenever the visibility state changes.

    • Callback logic: Inside the callback, check the inView boolean. If true, fire your tracking event (e.g., a Google Tag Manager dataLayer.push).
    • triggerOnce: Set to true to ensure the impression is only tracked once per page load.
    • threshold or rootMargin: Use these to define exactly when the element is considered "viewed".
    import * as React from "react";
    import { useOnInView } from "react-intersection-observer";
    
    const TrackImpression = () => {
      const ref = useOnInView((inView) => {
          if (inView) {
              // Fire a tracking event to your tracking service of choice.
              dataLayer.push("Section shown"); // Here's a GTM dataLayer push
          }
      }, {
        triggerOnce: true,
        rootMargin: "-100px 0",
      });
    
      return (
        <div ref={ref}>
          Exemplars sunt zeluss de bassus fuga. Credere velox ducunt ad audax amor.
        </div>
      );
    };
    
    export default TrackImpression;
  4. Trigger animations on viewport entry

    main

    Use the useInView hook to trigger CSS transitions or animations when an element enters the viewport.

    • triggerOnce: Set to true if you want the animation to fire only the first time the element becomes visible.
    • threshold: Use this to control how much of the element must be visible before the animation triggers.
    • rootMargin: Alternatively, use rootMargin to trigger the animation at a specific distance from the viewport edge. Using a negative margin (e.g., -100px 0px) will cause the animation to trigger only after the element has moved a certain distance into the viewport.
    import React from "react";
    import { useInView } from "react-intersection-observer";
    
    const LazyAnimation = () => {
      const { ref, inView } = useInView({
        triggerOnce: true,
        rootMargin: "-100px 0px",
      });
    
      return (
        <div
          ref={ref}
          className={`transition-opacity ${inView ? "opacity-1" : "opacity-0"}`}
        >
          <span aria-label="Wave">👋</span>
        </div>
      );
    };
    
    export default LazyAnimation;
  5. Install and use the IntersectionObserver polyfill

    main

    To support older browsers (like IE11 or older iOS versions), install and import the intersection-observer polyfill.

    Installation:

    yarn add intersection-observer

    Usage: Import it at the top level of your application. For performance, you can use dynamic imports to load it only when window.IntersectionObserver is undefined.

    async function loadPolyfills() {
      if (typeof window.IntersectionObserver === "undefined") {
        await import("intersection-observer");
      }
    }
    yarn add intersection-observer
    
    // In your app entry point
    import "intersection-observer";
    
    // Or via dynamic import for feature detection
    async function loadPolyfills() {
      if (typeof window.IntersectionObserver === "undefined") {
        await import("intersection-observer");
      }
    }
  6. Track element visibility with Intersection Observer v2 🧪

    main

    To track if an element is actually visible (not just intersecting, but not covered by other elements or obscured by filters), use the trackVisibility and delay options.

    Note: This requires browser support for Intersection Observer v2. If unsupported, the library falls back to reporting isVisible as true. You may need to manually extend the IntersectionObserverEntry type to include the isVisible boolean in your TypeScript configuration.

    const TrackVisible = () => {
      const { ref, entry } = useInView({ trackVisibility: true, delay: 100 });
      return <div ref={ref}>{entry?.isVisible}</div>;
    };
  7. Implement lazy image loading

    main

    You can create a custom lazy image loader by using the useInView hook. To ensure a smooth user experience and prevent layout shifts, follow these best practices:

    • Delay src assignment: Do not set the src or srcset on the <img> tag until inView is true. Images will load even if they are hidden with display: none;.
    • Use rootMargin: Set a positive rootMargin (e.g., 200px 0px) so the image begins loading before it actually enters the viewport.
    • Use triggerOnce: Set triggerOnce: true to stop monitoring the element once it has been loaded.
    • Maintain Aspect Ratio: Wrap the image in a container that preserves the aspect ratio using padding-bottom (calculated as (height / width) * 100%) to prevent layout jumps when the image loads.

    Note: For simple use cases, consider using the native browser loading="lazy" attribute instead.

    import React from "react";
    import { useInView } from "react-intersection-observer";
    
    const LazyImage = ({ width, height, src, ...rest }) => {
      const { ref, inView } = useInView({
        triggerOnce: true,
        rootMargin: "200px 0px",
      });
    
      return (
        <div
          ref={ref}
          style={{
            position: "relative",
            paddingBottom: `${(height / width) * 100}%`,
            background: "#2a4b7a",
          }}
        >
          {inView ? (
            <img
              {...rest}
              src={src}
              width={width}
              height={height}
              style={{ position: "absolute", width: "100%", height: "100%" }}
            />
          ) : null}
        </div>
      );
    };
    
    export default LazyImage;
  8. Mock the Intersection Observer API for testing

    main

    When testing components that use react-intersection-observer in environments like Jest or Vitest, you must mock the global IntersectionObserver API. Use setupIntersectionMocking to initialize the mock and resetIntersectionMocking to clear it between tests.

    If you are not using Jest or Vitest, you must manually call these functions in your test setup file.

    // test-setup.js
    import { resetIntersectionMocking, setupIntersectionMocking } from 'react-intersection-observer/test-utils';
    
    beforeEach(() => {
      setupIntersectionMocking(vi.fn);
    });
    
    afterEach(() => {
      resetIntersectionMocking();
    });
  9. Troubleshoot `rootMargin` issues

    main

    If rootMargin is not behaving as expected:

    1. Check the Root: rootMargin is applied to the root element. If your app is in an <iframe> or you have a custom root defined, the margin applies to that specific element, not necessarily the document viewport.
    2. Use scrollMargin: If the target is being clipped by a nested scrollable container inside the root, use the scrollMargin option instead of rootMargin to adjust the clipping rectangle of that container.
  10. Assign multiple refs to a single component

    main

    If you need to use both a local useRef and the ref provided by useInView on the same element, wrap the assignments in a useCallback to avoid recreating the function on every render.

    import React, { useRef, useCallback } from "react";
    import { useInView } from "react-intersection-observer";
    
    function Component(props) {
      const ref = useRef();
      const { ref: inViewRef, inView } = useInView();
    
      // Use `useCallback` so we don't recreate the function on each render
      const setRefs = useCallback(
        (node) => {
          // Ref's from useRef needs to have the node assigned to `current`
          ref.current = node;
          // Callback refs, like the one from `useInView`, is a function that takes the node as an argument
          inViewRef(node);
        },
        [inViewRef],
      );
    
      return <div ref={setRefs}>Shared ref is visible: {inView}</div>;
    }
  11. Track visibility without re-renders with useOnInView

    main

    The useOnInView hook is a performance-optimized alternative to useInView. It does not trigger component re-renders. Instead, it accepts a callback function that is executed whenever the element enters or leaves the viewport.

    Key characteristics:

    • No re-renders: Ideal for performance-critical scenarios (e.g., logging impressions).
    • Direct element access: The callback receives the IntersectionObserverEntry containing the target element.
    • Boolean-first callback: The callback signature is (inView, entry) => void.
    • Options: Accepts the same options as useInView except onChange, initialInView, and fallbackInView.

    Note: Like useInView, the initial false notification is skipped.

    import React from "react";
    import { useOnInView } from "react-intersection-observer";
    
    const Component = () => {
      // Track when element appears without causing re-renders
      const trackingRef = useOnInView(
        (inView, entry) => {
          if (inView) {
            // Element is in view - perhaps log an impression
            console.log("Element appeared in view", entry.target);
          } else {
            console.log("Element left view", entry.target);
          }
        },
        {
          /* Optional options */
          threshold: 0.5,
          triggerOnce: true,
        },
      );
    
      return (
        <div ref={trackingRef}>
          <h2 >This element is being tracked without re-renders</h2>
        </div>
      );
    };