Skip to content

use-multiple-intersection-observer

A React hook that provides a convenient way to observe multiple elements simultaneously using the Intersection Observer API. Built on top of useIntersectionObserver for consistent behavior and TypeScript support.

WARNING

This hook requires you to understand about useIntersectionHook. As it is built on top it, so it will help you to better understand about features of this hook. Read here useIntersectionHook

Features

  • Multiple observers: Create multiple intersection observers with a single hook call
  • Consistent API: Each observer follows the same pattern as useIntersectionObserver
  • Type-safe: Full TypeScript support with proper typing for all observer instances
  • Shared configuration: Apply the same options to all observers while maintaining individual keys
  • Auto cleanup: All observers are automatically cleaned up on unmount
  • Performance optimized: Each observer is independently managed for optimal performance

Parameters

ParameterTypeRequiredDefault ValueDescription
keysreadonly Key[]-Array of unique keys for creating named observers
optionsMultipleObserverOptionsundefinedShared configuration for all observers

Types

ts
export type MultipleObserverOptions = Omit<IntersectionObserverOptions, 'key'>

// Return type is a record where each key maps to its observer result
type MultipleIntersectionObserverResult<Key extends string> = Record<
   Key,
   ReturnType<typeof useIntersectionObserver<Key>>
>

Options Properties

All options from useIntersectionObserver except key (which is provided via the keys array):

PropertyTypeDefaultDescription
onIntersection(entry: IntersectionObserverEntry) => voidundefinedCallback fired on intersection changes
onlyTriggerOncebooleanfalseWhether to observe only the first intersection
rootElement | Document | nullnullRoot element for intersection
rootMarginstring'0px'Margin around root element
thresholdnumber | number[]0Intersection ratio threshold(s)

Return Value

The hook returns a record object where each key from the input array maps to its corresponding intersection observer result:

ts
{
  [key]: {
    [`${key}Element`]: HTMLElement | null,
    [`set${Capitalize<Key>}ElementRef`]: (element: HTMLElement | null) => void,
    [`is${Capitalize<Key>}ElementIntersecting`]: boolean
  }
}

Return Value

The hook returns a record object where each key from the input array maps to its corresponding intersection observer result:

  • Without key: element, setElementRef, isElementIntersecting
  • With key: {key}Element, set{Key}ElementRef, is{Key}ElementIntersecting
ts
// Object contains all of the obervers
{   
  [key]: {
    [`${key}Element`]: HTMLElement | null,
    [`set${Capitalize<Key>}ElementRef`]: (element: HTMLElement | null) => void,
    [`is${Capitalize<Key>}ElementIntersecting`]: boolean
  }
}  

INFO

{key}Element: Holds the element reference which is being observed, it's initial undefined.

set{Capitalize<key>}ElementRef: Setter function to store the element reference within element, which is going tobe observed.

is{Capitalize<key>}ElementIntersecting: Holds the boolean intersection status of the element weather it is intersecting the screen or not.

Usage Examples

Basic Multiple Observers

tsx
import { useMultipleIntersectionObserver } from 'classic-react-hooks'

export default function MultipleObserversExample() {
   const observers = useMultipleIntersectionObserver(['header', 'main', 'footer'] as const, {
      threshold: 0.3,
      onIntersection: (entry) => {
         console.log('Element intersection changed:', entry.target.id)
      },
   })

   return (
      <div>
         <header
            ref={observers.header.setHeaderElementRef}
            style={{
               height: '200px',
               backgroundColor: observers.header.isHeaderElementIntersecting ? 'lightblue' : 'gray',
               display: 'flex',
               alignItems: 'center',
               justifyContent: 'center',
            }}
         >
            <h1>Header {observers.header.isHeaderElementIntersecting ? '(Visible)' : '(Hidden)'}</h1>
         </header>

         <main
            ref={observers.main.setMainElementRef}
            style={{
               height: '100vh',
               backgroundColor: observers.main.isMainElementIntersecting ? 'lightgreen' : 'lightgray',
               display: 'flex',
               alignItems: 'center',
               justifyContent: 'center',
            }}
         >
            <h2>Main Content {observers.main.isMainElementIntersecting ? '(Visible)' : '(Hidden)'}</h2>
         </main>

         <footer
            ref={observers.footer.setFooterElementRef}
            style={{
               height: '200px',
               backgroundColor: observers.footer.isFooterElementIntersecting ? 'lightcoral' : 'darkgray',
               display: 'flex',
               alignItems: 'center',
               justifyContent: 'center',
            }}
         >
            <h3>Footer {observers.footer.isFooterElementIntersecting ? '(Visible)' : '(Hidden)'}</h3>
         </footer>
      </div>
   )
}
Details
tsx
import { useMultipleIntersectionObserver } from 'classic-react-hooks'

export default function NavigationTracker() {
   const sections = useMultipleIntersectionObserver(['hero', 'about', 'services', 'contact'] as const, {
      threshold: 0.5,
      rootMargin: '-20% 0px -20% 0px', // Only trigger when element is well within viewport
   })

   // Find the currently active section
   const activeSection =
      Object.entries(sections).find(
         ([key, observer]) => observer[`is${observer.constructor.name}ElementIntersecting` as keyof typeof observer]
      )?.[0] || null

   return (
      <div>
         {/* Sticky Navigation */}
         <nav
            style={{
               position: 'fixed',
               top: 0,
               width: '100%',
               backgroundColor: 'white',
               padding: '10px',
               borderBottom: '1px solid #ccc',
               zIndex: 1000,
            }}
         >
            {(['hero', 'about', 'services', 'contact'] as const).map((section) => (
               <button
                  key={section}
                  style={{
                     margin: '0 10px',
                     padding: '5px 15px',
                     backgroundColor: activeSection === section ? 'blue' : 'lightgray', 
                     color: activeSection === section ? 'white' : 'black', 
                     border: 'none',
                     borderRadius: '4px',
                  }}
               >
                  {section.charAt(0).toUpperCase() + section.slice(1)}
               </button>
            ))}
         </nav>

         {/* Sections */}
         <div style={{ marginTop: '60px' }}>
            <section
               ref={sections.hero.setHeroElementRef}
               style={{ height: '100vh', backgroundColor: '#ff6b6b', padding: '20px' }}
            >
               <h1>Hero Section</h1>
            </section>

            <section
               ref={sections.about.setAboutElementRef}
               style={{ height: '100vh', backgroundColor: '#4ecdc4', padding: '20px' }}
            >
               <h1>About Section</h1>
            </section>

            <section
               ref={sections.services.setServicesElementRef}
               style={{ height: '100vh', backgroundColor: '#45b7d1', padding: '20px' }}
            >
               <h1>Services Section</h1>
            </section>

            <section
               ref={sections.contact.setContactElementRef}
               style={{ height: '100vh', backgroundColor: '#f9ca24', padding: '20px' }}
            >
               <h1>Contact Section</h1>
            </section>
         </div>
      </div>
   )
}

Common Use Cases

  • Multi-section navigation: Track visibility of multiple page sections for active navigation states
  • Lazy loading galleries: Load multiple images or content blocks as they come into view
  • Analytics tracking: Monitor user engagement across different content areas
  • Animation choreography: Coordinate animations across multiple elements
  • Performance monitoring: Track which sections users actually view
  • Infinite scroll sections: Manage multiple loading zones in complex layouts

Performance Considerations

Important

Each key in the array creates a separate useIntersectionObserver instance. While this provides maximum flexibility, consider the performance impact when observing many elements simultaneously.

TIP

For better performance with many elements, consider grouping related observations or using a single observer with multiple targets if the behavior is identical.

TypeScript Benefits

The hook provides excellent TypeScript support with:

  • Readonly keys array: Ensures keys are treated as literal types for better inference
  • Mapped return types: Each key gets properly typed observer properties
  • Consistent API: All observers follow the same naming pattern as the base hook
tsx
// Type-safe usage
const observers = useMultipleIntersectionObserver(['hero', 'about'] as const)

// TypeScript knows these properties exist:
observers.hero.setHeroElementRef
observers.hero.isHeroElementIntersecting
observers.about.setAboutElementRef
observers.about.isAboutElementIntersecting

Relationship to useIntersectionObserver

Details

This hook is a thin wrapper around useIntersectionObserver that:

  • Creates multiple observer instances with the same configuration
  • Provides a convenient API for managing related observations
  • Maintains all the features and behavior of the base hook
  • Uses the same TypeScript patterns for consistent developer experience