使用 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>
);
}
这是一个可重复使用的钩子,它使用ref
和useEffect
清理功能来防止在安装/卸载大量组件时内存泄漏
挂钩
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
...
希望对大家有所帮助。
我正在尝试使用 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>
);
}
这是一个可重复使用的钩子,它使用ref
和useEffect
清理功能来防止在安装/卸载大量组件时内存泄漏
挂钩
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
...
希望对大家有所帮助。