iT邦幫忙

2022 iThome 鐵人賽

DAY 26
0
Modern Web

React Hook 不求人,建立自己的 Hook Libary系列 第 26

[DAY 26] 自己的Hook自己做! 用 IntersectionObserver 弄出動態的網站吧!

  • 分享至 

  • xImage
  •  

DEMO 在這裡

情境

面對長長的列表與龐大的資料時,為了避免使用者感受到卡卡,你可能會希望等使用者滾動到該卡片時再進行資料的抓取;或是你想依照元素呈現的比例進行不同的效果,這種事情可以交給 IntersectionObserver 處理!

功能及描述

如圖片呈現:

  • 當物體進入到 Viewport / 特定比例時,再進一步後續的動作

開始!

首先,需要一些內容:

<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

IntersectionObserver

基本上就是參照 MDN IntersectionObserver怎麼用我們就怎麼用 XD

以下段落為參照MDN的說明

let observer = new IntersectionObserver(callback, options);
observer.observe(target);
  • options 有三個屬性可以設定:
options = {
  root,
  rootMargin,
  threshold,
}
  • root HTMLElement,定義要觀察的範圍,預設就是瀏覽器的 viewport
  • rootMargin 如同CSS Margin,可以視為觀察範圍的調整,預設 0px
  • threshold number or array<number>,0 ~ 1.0 觀察的目標呈現的比例到達多少時會執行callback

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

透過文件了解之後,把他加入到 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 怎麼處理:

Yellow 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..(依照當下滾動情況)

Green Box

這個 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 有定義了 rootrootMargin,設定了 margin bottom 100px,也就是 root 的 viewport 往下延伸 100px,雖然不在「可視範圍」,但因交互範圍延伸會執行 callback。

Blue Box

我們可以基於這個 hook 再建立一個 useInView hook:

useInView
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} />

DEMO 在這裡

結語

命名都好長 (ノ・ェ・)ノ


上一篇
[DAY 25] 自己的Hook自己做!html2canvas 來擷圖網頁的內容吧!
下一篇
[DAY 27] 自己的Hook自己做!一些零碎的 hook 們
系列文
React Hook 不求人,建立自己的 Hook Libary30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言