React useOnScreen Hook Challenge
This challenge asks you to create a custom React hook, useOnScreen, that determines whether a given DOM element is currently visible within the user's viewport. This is a fundamental hook for implementing features like lazy loading, infinite scrolling, and scroll-triggered animations, significantly improving user experience and performance.
Problem Description
You need to develop a TypeScript React hook named useOnScreen. This hook will take a ref to a DOM element as an argument and return a boolean value indicating whether that element is currently visible within the browser's viewport.
Key Requirements:
- The hook should accept a
React.RefObject<T>whereTis a DOM element type (e.g.,HTMLDivElement). - The hook should return
trueif any part of the referenced element is visible in the viewport, andfalseotherwise. - The hook should update its return value in real-time as the user scrolls or resizes the window.
- The hook should clean up any event listeners when the component unmounts to prevent memory leaks.
Expected Behavior:
When a component uses useOnScreen with a ref to an element, the boolean returned by the hook should accurately reflect the element's visibility. For example, if the element is off-screen, the hook should return false. As the user scrolls the page and the element enters the viewport, the hook should return true.
Edge Cases:
- The initial state of the element when the component first renders.
- The element might be initially off-screen but become visible later.
- The element might be partially visible.
- Window resizing can affect visibility.
- The provided
refmight be null or undefined initially.
Examples
Example 1: Basic Usage
Input (Conceptual):
Imagine a component that renders a div and passes its ref to useOnScreen.
import React, { useRef } from 'react';
import useOnScreen from './useOnScreen'; // Assume this is your hook
function MyComponent() {
const elementRef = useRef<HTMLDivElement>(null);
const isVisible = useOnScreen(elementRef);
return (
<div style={{ height: '100vh' }}> {/* Placeholder to create scroll */}
<div
ref={elementRef}
style={{
height: '200px',
width: '200px',
backgroundColor: isVisible ? 'lightgreen' : 'lightcoral',
marginTop: '50vh', // Position to be initially off-screen
}}
>
{isVisible ? 'I am visible!' : 'I am hidden.'}
</div>
</div>
);
}
Output (Conceptual):
- When the component initially loads and the
divis below the fold (not in the viewport):isVisiblewill befalse.- The
divwill have alightcoralbackground and display "I am hidden."
- When the user scrolls down and the
diventers the viewport:isVisiblewill becometrue.- The
divwill have alightgreenbackground and display "I am visible!".
Example 2: Handling Initial Visibility
Input (Conceptual):
A component where the target div is initially within the viewport.
import React, { useRef } from 'react';
import useOnScreen from './useOnScreen';
function AnotherComponent() {
const elementRef = useRef<HTMLDivElement>(null);
const isVisible = useOnScreen(elementRef);
return (
<div style={{ height: '50vh' }}> {/* Just enough space for some content */}
<div
ref={elementRef}
style={{
height: '100px',
width: '100px',
backgroundColor: isVisible ? 'lightblue' : 'lightgray',
position: 'fixed', // Keep it in view
top: '10px',
left: '10px',
}}
>
{isVisible ? 'Always visible!' : 'Should not be hidden.'}
</div>
</div>
);
}
Output (Conceptual):
- Upon initial render, the
divis within the viewport.isVisiblewill betrue.- The
divwill have alightbluebackground and display "Always visible!".
Constraints
- The hook must be implemented in TypeScript.
- The hook should leverage the
IntersectionObserverAPI for optimal performance. - Avoid using direct
window.addEventListener('scroll', ...)calls ifIntersectionObservercan be used instead. - The hook should be performant, especially when used with many elements.
Notes
- Consider using
IntersectionObserveras it is the modern and efficient way to detect element visibility. - The
refpassed to the hook might not have itscurrentproperty available immediately on the first render. Your hook should handle this gracefully. - Think about how to manage the observer instance (creation and cleanup).
- The
IntersectionObserverAPI provides options likerootMarginandthresholdthat you might want to consider exposing or using default values for. For this challenge, focusing on basic visibility is sufficient.