使用 React 和 Hooks 的 IntersectionObserver

IntersectionObserver with React & Hooks

我正在尝试使用 React/Hooks 和 Intersection Observer API 来跟踪元素可见性。但是,我不知道如何使用 "useEffect" 设置观察。有谁知道我该怎么做?我的解决方案不起作用...

function MainProvider({ children }) {
  const [targetToObserve, setTargetToObserve] = useState([]);

  window.addEventListener("load", () => {
    const findTarget = document.querySelector("#thirdItem");
    setTargetToObserve([findTarget]);
  });

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.intersectionRatio === 0.1) {
          console.log("It works!");
        }
      },
      {
        root: null,
        rootMargin: "0px",
        threshold: 0.1
      }
    );
    if (targetToObserve.current) {
      observer.observe(targetToObserve.current);
    }
  }, []);

  return (
    <main>
     <div className="Section-item" id="firstItem"></div>
     <div className="Section-item" id="secondItem"></div>
     <div className="Section-item" id="thirdItem"></div>
    </main>
  );
}

需要使用 React.useRef() 而不是 addEventListener('load', function() ),因为 eventListener 将 运行 在某些内容出现在屏幕上之前。

import React, { useRef, useEffect } from 'react'

function MainProvider({ children }) {
  const ref = useRef();

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        console.log(entry);

        if (entry.isIntersecting) {
          //do your actions here
          console.log('It works!')
        }
      },
      {
        root: null,
        rootMargin: "0px",
        threshold: 0.1
      }
    );
    if (ref.current) {
      observer.observe(ref.current);
    }
  }, [ref]);

  return (
    <main>
     <div className="Section-item" id="firstItem"></div>
     <div className="Section-item" ref={ref} id="secondItem"></div>
     <div className="Section-item" id="thirdItem"></div>
    </main>
  );
}

这是一个可重复使用的钩子,它使用refuseEffect清理功能来防止在安装/卸载大量组件时内存泄漏

挂钩

function useOnScreen(ref) {

  const [isIntersecting, setIntersecting] = useState(false)

  const observer = new IntersectionObserver(
    ([entry]) => setIntersecting(entry.isIntersecting)
  )

  useEffect(() => {
    observer.observe(ref.current)
    return () => {
      observer.disconnect()
    }
  }, [])

  return isIntersecting
}

组件中的用法

function DumbComponent() {

  const ref = useRef()

  const onScreen = useOnScreen(ref)

  return <div ref={ref}>{onScreen && "I'm on screen!"}</div>
}

JavaScript

挂钩

import { useEffect, useState, useRef } from 'react';

export function useOnScreen(ref) {
  const [isOnScreen, setIsOnScreen] = useState(false);
  const observerRef = useRef(null);

  useEffect(() => {
    observerRef.current = new IntersectionObserver(([entry]) =>
      setIsOnScreen(entry.isIntersecting)
    );
  }, []);

  useEffect(() => {
    observerRef.current.observe(ref.current);

    return () => {
      observerRef.current.disconnect();
    };
  }, [ref]);

  return isOnScreen;
}


用法:

import { useRef } from 'react';
import useOnScreen from './useOnScreen';

function MyComponent() {
  const elementRef = useRef(null);
  const isOnScreen = useOnScreen(elementRef);

  console.log({isOnScreen});

  return (
    <div>
      <div style={{ paddingBottom: '140vh' }}>scroll to element...</div>
      <div ref={elementRef}>my element</div>
    </div>
  );
}

TypeScript

挂钩

import { useEffect, useState, useRef, RefObject } from 'react';

export default function useOnScreen(ref: RefObject<HTMLElement>) {
  const observerRef = useRef<IntersectionObserver | null>(null);
  const [isOnScreen, setIsOnScreen] = useState(false);

  useEffect(() => {
    observerRef.current = new IntersectionObserver(([entry]) =>
      setIsOnScreen(entry.isIntersecting)
    );
  }, []);

  useEffect(() => {
    observerRef.current.observe(ref.current);

    return () => {
      observerRef.current.disconnect();
    };
  }, [ref]);

  return isOnScreen;
}

用法:

import { useRef } from 'react';
import useOnScreen from './useOnScreen';

function MyComponent() {
  const elementRef = useRef<HTMLDivElement>(null);
  const isOnScreen = useOnScreen(elementRef);

  console.log({isOnScreen});

  return (
    <div>
      <div style={{ paddingBottom: '140vh' }}>scroll to element...</div>
      <div ref={elementRef}>my element</div>
    </div>
  );
}

https://codesandbox.io/s/useonscreen-uodb1?file=/src/useOnScreen.ts

从您的示例来看,您似乎只需要在初始渲染时设置一次观察器。

function MainProvider({ children }) {
  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.intersectionRatio === 0.1) {
          console.log("It works!");
        }
      },
      {
        root: null,
        rootMargin: "0px",
        threshold: 0.1
      }
    );

    const findTarget = document.querySelector("#thirdItem");

    if (findTarget) {
      observer.observe(targetToObserve.current);
    }
  }, []);

  return (
    <main>
     <div className="Section-item" id="firstItem"></div>
     <div className="Section-item" id="secondItem"></div>
     <div className="Section-item" id="thirdItem"></div>
    </main>
  );
}

但是,如果您有其他依赖项需要您添加或删除更多您正在观察的元素,您可以将您的观察者放在 useEffect 挂钩中,确保包含您所观察的事物的依赖项正在尝试观察。

如果你也将你的观察者放在你的依赖数组中(就像你的 linter 可能建议的那样)你会得到一个有用的错误消息,告诉你将在每次渲染时创建一个新的观察者对象,触发这个钩子运行 在每个渲染器上。相反,它建议您将观察者放在 useMemo 钩子中,建议将其用于昂贵的计算。

function MainProvider({ children }) {
  const observer = useMemo(() => return new IntersectionObserver(
    ([entry]) => {
      if (entry.intersectionRatio === 0.1) {
        console.log("It works!");
      }
    },
    {
      root: null,
      rootMargin: "0px",
      threshold: 0.1
    }
  );
 );

  useEffect(() => {
    const findTarget = document.querySelector("#thirdItem");

    if (targetToObserve.current) {
      observer.observe(targetToObserve.current);
    }
  }, [observer]);

  return (
    <main>
     <div className="Section-item" id="firstItem"></div>
     <div className="Section-item" id="secondItem"></div>
     <div className="Section-item" id="thirdItem"></div>
    </main>
  );
}

补充 Filip Szczepanski 的答案,我发现它工作得很好,除非你需要你的元素有条件地呈现,这在你需要进行 API 调用时很糟糕,例如(基于 Filip 示例的代码):

import { useRef } from 'react';
import useOnScreen from './useOnScreen';

const fakeApiFetch = () => {
  return Promise.resolve(
    [
      {
        id: 0,
        name: 'Wash the dishes'
      },
      {
        id: 1,
        name: 'Make the bed'
      }
    ]
  );
}

function MyComponent() {
  const [data, setData] = useState<Array<any>>([]);
  const elementRef = useRef<HTMLDivElement>(null);
  const isOnScreen = useOnScreen(elementRef);

  useEffect(() => {
     (async() => {
         const res = await fakeApiFetch();
         setData(res);
         console.log(res);
     })();
  }, []);

  return (
    data.length > 0? (
      <div>
        <div style={{ paddingBottom: '140vh' }}>scroll to element...</div>
        <div ref={elementRef}>my element</div>
      </div>
    ) : (
      <h3>Fetching data...</h3>
    )
  );
}

此代码将不起作用,IntersectionObserver 似乎不再找到该元素并且在 data 收到来自 API 的数据后不会自行更新。

可以做什么:

export default function useOnScreen(
  ref: RefObject<HTMLElement>,
  triggers: Array<any> = [] // Add triggers
) {
  const [isOnScreen, setIsOnScreen] = useState(false);
  const observerRef = useRef<IntersectionObserver>();

  useEffect(() => {
    observerRef.current = new IntersectionObserver(([entry]) =>
      setIsOnScreen(entry.isIntersecting)
    );
  }, []);

  useEffect(() => {
    if (!!observerRef.current && !!ref.current) {
      observerRef.current.observe(ref.current);

      return () => {
        observerRef.current!.disconnect();
      };
    }
  }, [ref, ...triggers]); // Let the triggers fire the effect too on changes

  return isOnScreen;
}

并且:

function MyComponent() {
  const [data, setData] = useState<Array<any>>([]);
  const elementRef = useRef<HTMLDivElement>(null);
  const isOnScreen = useOnScreen(elementRef, [data]);
                                             // ^
                                             // | Add this
  ...

希望对大家有所帮助。