面對長長的列表與龐大的資料時,為了避免使用者感受到卡卡,你可能會希望等使用者滾動到該卡片時再進行資料的抓取;或是你想依照元素呈現的比例進行不同的效果,這種事情可以交給 IntersectionObserver 處理!
如圖片呈現:
首先,需要一些內容:
<Stack maxH="300px" overflowY="scroll">
<Item />
<Item />
<Item bg="yellow"/>
<Item bg="green.500"/>
<Item bg="blue.500" />
<Item />
<Item />
</Stack>
<Item />
大致內容如最上面呈現的圖片,預設是一個 200x200 的粉色正方形:
Item
const Item = forwardRef((props, ref) => {
return (
<Box
w="200px"
minH="200px"
ref={ref}
bg="salmon"
{...props}
position="relative"
transition="background-color 0.2s"
>
<Pointer top="0%" text="0" />
<Pointer top="50%" text="0.5" />
<Pointer top="100%" text="1.0" />
</Box>
)
})
待會會用到 ref,這邊先把 forwardRef
以及 ref
安好。
bg 是 Chakra-UI 提供 background 的 shorthand prop
blue.500/green.500 是 Chakra-UI 的 color token
基本上就是參照 MDN IntersectionObserver怎麼用我們就怎麼用 XD
以下段落為參照MDN的說明
let observer = new IntersectionObserver(callback, options);
observer.observe(target);
options
有三個屬性可以設定:options = {
root,
rootMargin,
threshold,
}
root
HTMLElement,定義要觀察的範圍,預設就是瀏覽器的 viewportrootMargin
如同CSS Margin,可以視為觀察範圍的調整,預設 0pxthreshold
number or array<number>,0 ~ 1.0 觀察的目標呈現的比例到達多少時會執行callback執行的 callback 大致如下:
let callback = (entries, observer) => {
entries.forEach((entry) => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
entry 代表每當 observer 觀察到目標進入範圍時,會提供該 intersection 變化的內容,這次主要會用到的是:
entry.isIntersecting
: 指目標與範圍有沒有在範圍裡面 (一部分也會是 true)entry.intersectionRatio
: 指目標有多少內容呈現在範圍裡面,值為目標的比例 0 ~ 1.0透過文件了解之後,把他加入到 hook:
function useIntersectionObserver(cb, options) {
const observerRef = useRef(null)
const targetRef = useRef(null)
useEffect(() => {
if (window) {
observerRef.current = new IntersectionObserver(cb, {
...options,
root: options?.root?.current || null,
})
if (targetRef.current) {
observerRef.current.observe(targetRef.current)
}
}
return () => {
observerRef.current.unobserve(targetRef.current)
}
}, [])
return targetRef
}
由於 IntersectionObserver 指存在瀏覽器上,確保 window 出現才會進行使用。
那來看看 DEMO 中黃藍綠的 Box 怎麼處理:
這個 Box 會依照呈現比例進行變色,設定 threshold
來定義 [0, 0.5, 1]
進行分段的變色,每當目標的內容呈現比例到達這些數值時,會進行觸發:
const [colorToken, setColorToken] = useState(".100")
const yellowBoxRef = useIntersectionObserver(handleYellowBoxIntersection, {
threshold: [0, 0.5, 1],
})
function handleYellowBoxIntersection(entries, observer) {
entries.forEach((entry) => {
const ratio = entry.intersectionRatio
let token = ""
if (ratio >= 0) {
token = ".100"
}
if (ratio >= 0.5) {
token = ".400"
}
if (ratio === 1) {
token = ".700"
}
setColorToken(token)
})
}
<Item bg={`yellow` + colorToken} ref={yellowBoxRef} />
用 >=
是因為滾動時不一定會每次剛好滾到該比例,以 threshold:0.5
來舉例,intersectionRatio
得出來的數值大約會是 0.500254..
(依照當下滾動情況)
這個 Box 我們來觀察有沒有進入 viewport:
const [isGreenBoxAround, setIsGreenBoxAround] = useState(false)
const greenBoxRef = useIntersectionObserver(handleGreenBoxIntersection, {
root: containerRef,
rootMargin: "0px 0px 100px 0px",
})
function handleGreenBoxIntersection(entries, observer) {
entries.forEach((entry) => {
setIsGreenBoxAround(entry.isIntersecting)
})
}
<Item bg="green.500" ref={greenBoxRef} />
透過 isIntersection 可以知道 Box 有沒有進入範圍,不過 DEMO 有定義了 root
與 rootMargin
,設定了 margin bottom 100px
,也就是 root 的 viewport 往下延伸 100px
,雖然不在「可視範圍」,但因交互範圍延伸會執行 callback。
我們可以基於這個 hook 再建立一個 useInView hook:
function useInView() {
const [isInView, setIsInView] = useState(null)
const handleInView = (entries, observe) => {
entries.forEach((entry) => {
setIsInView(entry.isIntersecting)
})
}
const targetRef = useIntersectionObserver(handleInView)
return [targetRef, isInView]
}
const [blueBoxRef, isBlueBoxInView] = useInView()
<Item bg="blue.500" ref={blueBoxRef} />
命名都好長 (ノ・ェ・)ノ