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:
- Input: The hook should accept a
React.RefObject<Element>as its primary argument. - Output: The hook should return a boolean value:
trueif the observed element is intersecting the viewport,falseotherwise. - Observer Configuration: The hook should allow for optional configuration of the
IntersectionObserver'sroot,rootMargin, andthresholdoptions. - 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.
- 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
RefObjectbecomes visible within the viewport (or its configuredroot), the hook should returntrue. - 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
RefObjectisnullorundefined? - What happens if the
IntersectionObserverAPI is not supported by the browser? - How does the hook handle re-renders where the
RefObjectmight 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
IntersectionObserverin 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
IntersectionObserverlogic.
Notes
- The
IntersectionObserverAPI is well-suited for this task, but it's important to handle its lifecycle correctly within a React component. - Consider using
useEffectfor 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
IntersectionObserveris 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
rootoption ofIntersectionObserverdefaults to the browser viewport if not provided. - The
rootMarginallows you to expand or shrink the area around therootelement. - The
thresholdis a number or an array of numbers indicating at what percentage of the target's visibility the observer's callback should be executed.