preface

This article uses React Hooks to build a Custom hook that listens for DOM elements to be exposed.

In the past, we used to listen for DOM elements by listening for scroll events to see if the target element is in the viewable area, so we need to get some data about the target element. This seems cumbersome, but using Intersection Observer to do this is more convenient and friendly.

Intersection Observer

The MDN: IntersectionObserver interface (part of the IntersectionObserver API) provides a way to asynchronously observe the intersecting status of a target element with its ancestor element or top-level document viewport. Ancestor elements and viewports are called roots.

Other relevant concepts can be seen in the discussion on IntersectionObserver lazy loading and IntersectionObserver API usage tutorial

Simply put, the object looks at the target element and fires a callback when the target element intersects with its ancestor or viewable area.

Through this object we can listen to the elements that need to be exposed to dot, easier to achieve dot. At the same time, we do not need to listen for scroll events and corresponding DOM operations, reducing performance overhead.

Intersection Observer practice in React

To be able to use it in different scenarios, we can wrap it as HOOKS to make it easier to call this method

1. Define the useIntersectionObserver function

// useIntersectionObserver.ts
// Define parameter function types and return value types

import { useState, useCallback, useEffect } from 'react'

type NumberList = number[]
type ObserverList = Array<React.RefObject<any>>
type CallbackFunction = (indexList: NumberList) = > void
type ResultType = [React.Dispatch<React.SetStateAction<React.RefObject<any> [] > >]function UseIntersectionObserver (
  observerList: ObserverList,
  callback: CallbackFunction,
  infinite: boolean = false,
  opt: IntersectionObserverInit = {}
) :ResultType {
  // list is a list of elements to listen on. SetList, as the return value of UseIntersectionObserver, allows the caller to modify the list to be monitored
  const [list, setList] = useState<ObserverList>(observerList)

  // intersectionObserver: specifies the observer object
  let intersectionObserver: IntersectionObserver | null = null
  
  // ...
  return [setList]
}

const useIntersectionObserver = UseIntersectionObserver

export default useIntersectionObserver
Copy the code

UseIntersectionObserver function parameters:

  • ObserverList: An array of observed objects. The entries are objects constructed from React. CreateRef
  • Callback: A function that needs to be triggered when the target element is exposed. This function takes an indexList argument consisting of the index of the exposed element in the observerList array
  • Infinite: Whether to continuously observe the target element. The default value is false. (Because exposure is usually reported only once)
  • Opt: You can customize exposure conditions (value composition reference MDN). The default is {threshold: [1]}. The callback is triggered only when the target element is fully exposed to the visible area

UseIntersectionObserver Return value:

  • Returns an array whose first element is returned by useState of React.

2. Implement listening

Define the observeExposure function

// UseIntersectionObserver

const observeExposure = useCallback((list: ObserverList) = > {}, [])
Copy the code

Use useCallback to reduce unnecessary duplicate function declarations

Check whether the browser environment and list are empty

   if(! IntersectionObserver) {throw new Error('Current browser does not support IntersectionObserver ')}if (list.length === 0) return
Copy the code
  • The target major browsers already support the object, but some older browsers are still compatible
  • Return when list is empty

Construct a new observer instance

   
   // Destroy the object while the observer exists
   intersectionObserver && intersectionObserver.disconnect()
   // Construct a new observer instance
   intersectionObserver = new IntersectionObserver(entries= > {
     // Save the exposed elements of this listen
     let activeList: NumberList = []
     
     If the exposure condition is visible, the callback function will be called and the listener will be cancelled
     entries.forEach(entrie= > {
       // Find the index of the listener in the list
       const index = Array.from(list).findIndex(
         item= > item.current === entrie.target
       )
       // Prevent accidents
       if (index === - 1) return
       
       // isIntersecting is the inherent attribute of each monitored element. If true, it indicates exposure
       // And has not been exposed
       if (entrie.isIntersecting) {
       
         // Save the exposure element index
         activeList.push(index)

         // Cancel the watch, if unlimited watch is required, do not cancel the listen! infinite && intersectionObserver && intersectionObserver.unobserve(list[index].current) } })/ / callback function
     activeList.length > 0 && callback(activeList)
   }, opt)
   
Copy the code

IntersectionObserver is used to listen on elements in the list

    // Look at each element recursively
    list.forEach(item= > {
      item.current &&
        intersectionObserver &&
        intersectionObserver.observe(item.current)
        
      // It is compatible with passing directly into DOM nodes.
      // if((<React.RefObject<any>>item).current) {
      // intersectionObserver.observe((
      
       >item).current)
      
      // } else if ((<HTMLElement>item)) {
      // intersectionObserver.observe((
      
       item))
      
      // }
    })
Copy the code

3. When the exposed setList is called each time, the list will be changed, and the listener needs to listen again

  useEffect((a)= > {
    observeExposure(list)
    
    // Unlink while unmounting
    return (a)= > {
      intersectionObserver && intersectionObserver.disconnect()
    }
  }, [list])
Copy the code

4. Complete code implementation

import { useState, useCallback, useEffect } from 'react'

type NumberList = number[]
type ObserverList = Array<React.RefObject<any>>
type CallbackFunction = (indexList: NumberList) = > void
type ResultType = [React.Dispatch<React.SetStateAction<React.RefObject<any> [] > >]/** * UseIntersectionObserver * @param observerList An array of observed targets, The function that needs to be fired when the target element is exposed. This function takes an indexList, Consists of the index of the exposed element in the observerList array * @param Infinite continuously observes the target element. The default value is false. By default, {threshold: [1]} will trigger a callback */ only if the target element is fully exposed
function UseIntersectionObserver (
  observerList: ObserverList,
  callback: CallbackFunction,
  infinite: boolean = false,
  opt: IntersectionObserverInit = {}
) :ResultType {
  // list is a list of elements to listen on. SetList, as the return value of UseIntersectionObserver, allows the caller to modify the list to be monitored
  const [list, setList] = useState<ObserverList>(observerList)

  // intersectionObserver: specifies the observer object
  let intersectionObserver: IntersectionObserver | null = null

  const observeExposure = useCallback((list: ObserverList) = > {
    if (typeof IntersectionObserver === 'undefined') {
      throw new Error('Current browser does not support IntersectionObserver ')}if (list.length === 0) return
    // Destroy the object while the observer exists
    intersectionObserver && intersectionObserver.disconnect()
    // Construct a new observer instance
    intersectionObserver = new IntersectionObserver(entries= > {
      // Save the exposed elements of this listen
      let activeList: NumberList = []

      If the exposure condition is visible, the callback function will be called and the listener will be cancelled
      entries.forEach(entrie= > {
        // Find the index of the listener in the list
        const index = Array.from(list).findIndex(
          item= > item.current === entrie.target
        )
        // Prevent accidents
        if (index === - 1) return

        // isIntersecting is the inherent attribute of each monitored element. If true, it indicates exposure
        // And has not been exposed
        if (entrie.isIntersecting) {
          // Save the exposure element index
          activeList.push(index)

          // Cancel the watch, if unlimited watch is required, do not cancel the listen! infinite && intersectionObserver && intersectionObserver.unobserve(list[index].current) } })/ / callback function
      activeList.length > 0 && callback(activeList)
    }, opt)

    list.forEach(item= > {
      item.current &&
        intersectionObserver &&
        intersectionObserver.observe(item.current)

      // It is compatible with passing directly into DOM nodes.
      // if((<React.RefObject<any>>item).current) {
      // intersectionObserver.observe((
      
       >item).current)
      
      // } else if ((<HTMLElement>item)) {
      // intersectionObserver.observe((
      
       item))
      
      // }
    })
  }, [])

  useEffect((a)= > {
    observeExposure(list)

    // Unlink while unmounting
    return (a)= > {
      intersectionObserver && intersectionObserver.disconnect()
    }
  }, [list])

  return [setList]
}

const useIntersectionObserver = UseIntersectionObserver

export default useIntersectionObserver

Copy the code


Use case

Implement a simple list of goods exposure dot case

import Card from 'components/goods-card/goods-card' import { connect } from 'react-redux' import { getSinglePromotionList } from '.. /.. /page_components/promotion/redux/creator' import React, { useEffect, useState, useCallback } from 'react' import useIntersectionObserver from 'page_components/promotion/useIntersectionObserver' const  List = (props: { info: any; getData: Any}) => {const {info, getData} = props // List of listened elements const [refList, setRefList] = useState<React.RefObject<any>[]>([]) const callback = useCallback((indexList: Number []) => {console.log(indexList)}, []) // Call const [setList] = useIntersectionObserver(refList, UseEffect (() => {setList(refList)}, [refList]) useEffect(() => {setList(refList)}, [refList]) RefList useEffect(() => {const list: React.RefObject<any>[] = info.list.map(() => react.createref ()) setRefList(list)}, [info]) UseEffect (() => {getData()}, []) return (<div style={{display: 'flex', flexWrap: 'wrap' }}> {info.list.map((item: any, index: number) => ( <div ref={refList[index]} key={index}> <Card card={item} /> </div> ))} </div> ) } const mapStateToProps = (state: any) => { return { info: state.promotionStore.singlePromotionInfo, userInfo: state.userInfo } } const mapDispatchToProps = (dispatch: any) => { return { getData: () => dispatch(getSinglePromotionList(params, silence)) } } export default connect( mapStateToProps, mapDispatchToProps )(List)Copy the code

Effect of case

As you can see from the driven figure, the index value is printed when card is exposed, and the value that has been exposed is not exposed again.





In this paper, the ~

If you have any questions, please point out ~