Hone logo
Hone
Problems

Create a Reusable useIntersection Hook in React

In modern web development, detecting when an element enters or leaves the viewport is a common requirement for features like lazy loading images, infinite scrolling, and triggering animations. This challenge asks you to build a custom React hook that leverages the browser's Intersection Observer API to efficiently manage these visibility changes.

Problem Description

Your task is to create a custom React hook called useIntersection that takes a React RefObject pointing to a DOM element and returns a boolean indicating whether that element is currently intersecting with the viewport.

Key Requirements:

  1. Input: The hook should accept a React.RefObject<Element> as its primary argument.
  2. Output: The hook should return a boolean value: true if the observed element is intersecting the viewport, false otherwise.
  3. Observer Configuration: The hook should allow for optional configuration of the IntersectionObserver's root, rootMargin, and threshold options.
  4. Cleanup: The hook must properly unobserve the element and disconnect the observer when the component unmounts or the observed element changes to prevent memory leaks.
  5. Type Safety: The solution must be written in TypeScript, ensuring type safety for all inputs and outputs.

Expected Behavior:

  • Initially, the hook should return false.
  • When the element referenced by the RefObject becomes visible within the viewport (or its configured root), the hook should return true.
  • When the element is no longer visible, the hook should return false.
  • The hook should update its returned boolean value reactively as the element's visibility changes.

Edge Cases:

  • What happens if the RefObject is null or undefined?
  • What happens if the IntersectionObserver API is not supported by the browser?
  • How does the hook handle re-renders where the RefObject might point to a different element?

Examples

Example 1: Basic Usage

import React, { useRef } from 'react';
import { useIntersection } from './useIntersection'; // Assuming your hook is in './useIntersection'

function MyComponent() {
  const elementRef = useRef<HTMLDivElement>(null);
  const isIntersecting = useIntersection(elementRef);

  return (
    <div>
      <div style={{ height: '100vh', backgroundColor: 'lightblue' }}>
        Scroll down...
      </div>
      <div
        ref={elementRef}
        style={{
          height: '200px',
          backgroundColor: isIntersecting ? 'lightgreen' : 'lightcoral',
          transition: 'background-color 0.3s ease',
        }}
      >
        This element changes color when visible!
      </div>
      <div style={{ height: '100vh' }}>More content</div>
    </div>
  );
}

Input: (Implicitly, the elementRef pointing to the div with changing background color) Output: The isIntersecting variable will be false when the "This element changes color..." div is out of view, and true when it enters the viewport. The background color of the div will change accordingly.

Example 2: With Observer Options

import React, { useRef } from 'react';
import { useIntersection } from './useIntersection';

function AnotherComponent() {
  const elementRef = useRef<HTMLDivElement>(null);
  const isIntersecting = useIntersection(elementRef, {
    rootMargin: '100px 0px', // Trigger when 100px above the element is visible
    threshold: 0.5, // Trigger when 50% of the element is visible
  });

  return (
    <div>
      <div style={{ height: '150vh' }}>Scroll down to trigger...</div>
      <div
        ref={elementRef}
        style={{
          height: '300px',
          backgroundColor: 'yellow',
          opacity: isIntersecting ? 1 : 0.5,
          transition: 'opacity 0.5s ease',
        }}
      >
        This element becomes more opaque when 50% visible and the root margin is considered.
      </div>
    </div>
  );
}

Input: The elementRef and custom rootMargin and threshold options. Output: The isIntersecting variable will become true only when at least 50% of the element is visible and when there's 100px of space above the element within the viewport. The opacity of the div will change.

Example 3: Handling RefObject Change

import React, { useRef, useState } from 'react';
import { useIntersection } from './useIntersection';

function DynamicComponent() {
  const [showFirst, setShowFirst] = useState(true);
  const firstDivRef = useRef<HTMLDivElement>(null);
  const secondDivRef = useRef<HTMLDivElement>(null);

  // The hook will automatically switch to observing the new ref if it changes
  const isIntersectingFirst = useIntersection(showFirst ? firstDivRef : secondDivRef);

  return (
    <div>
      <button onClick={() => setShowFirst(!showFirst)}>Toggle Element</button>
      <div style={{ height: '80vh' }}>Spacer</div>

      {showFirst && (
        <div
          ref={firstDivRef}
          style={{ height: '100px', backgroundColor: isIntersectingFirst ? 'cyan' : 'gray' }}
        >
          First Element
        </div>
      )}

      {!showFirst && (
        <div
          ref={secondDivRef}
          style={{ height: '100px', backgroundColor: isIntersectingFirst ? 'magenta' : 'gray' }}
        >
          Second Element
        </div>
      )}
      <div style={{ height: '80vh' }}>Spacer</div>
    </div>
  );
}

Input: The component toggles which element is rendered and referenced by the useIntersection hook. Output: The isIntersectingFirst variable correctly reflects the visibility of whichever element is currently being passed to the hook via the RefObject.

Constraints

  • The hook must be written in TypeScript.
  • The hook should ideally have a runtime performance comparable to directly using IntersectionObserver in a component.
  • The hook should be compatible with React versions that support hooks (e.g., React 16.8+).
  • Do not use any third-party libraries for implementing the core IntersectionObserver logic.

Notes

  • The IntersectionObserver API is well-suited for this task, but it's important to handle its lifecycle correctly within a React component.
  • Consider using useEffect for setting up and tearing down the observer.
  • Think about how to manage the state (the boolean isIntersecting) within the hook and ensure it updates appropriately.
  • For browser compatibility, while the IntersectionObserver is widely supported, you might consider a fallback or graceful degradation strategy if targeting very old browsers, although this is not a primary requirement for this challenge.
  • The root option of IntersectionObserver defaults to the browser viewport if not provided.
  • The rootMargin allows you to expand or shrink the area around the root element.
  • The threshold is a number or an array of numbers indicating at what percentage of the target's visibility the observer's callback should be executed.
Loading editor...
typescript