Briefly looking at IntersectionObserver

What is IntersectionObserver?

IntersectionObserver is web API.

IO API is watching target element and the upper top element or the very top element’s changes between viewport and intersection asynchronously.

mdn source

The reason why we use IntersectionObserver

Traditionally, we used to detect scrolling of HTML element by add event listener on window scroll event

But window’s scroll event listener has some cons.

  • Scroll event listener’s callback will be over-called which we do not need. (But, in this case, we can optimize this issue by reducing calling rate by debounce or throttle)
  • If we want to watch specific area, we have to use getBoundingClientRect() function. When we use this function, reflow will be triggered. So the whole browser or the specific area’s whole part would be repainted.
Reflow: Reflow is triggered when the browser has to repaint the whole browser or some part of the browser

So, this kind of traditional listener solution needs to be optimezed.

That’s why Intersection Observer API was invented!

It is asynchronous function, so it doesn’t affect to main thread’s performance while they are watching changes.

Also, if you try IntersectionObserverEntry , you can get same result as getBoundingClientRect().

So you don’t have to run getBoundingClientRect()function individually.

As a result, you can prevent Reflow

Source

Let’s try IntersectionObserver (briefly)

const observer = new IntersectionObserver(callback, options)

Constructor code above.

So then, let’s look at callback and options paramter.

IntersectionObserver: 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
  });
};

IntersectionObserver: options

let options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}

let observer = new IntersectionObserver(callback, options);
  • threshold: 1.0 means 100% visibility of the viewport from root element. If you want 25% visibility, you can set it to 0.25. Or you can also steps as array like [0, 0.25, 0.5, 0.75, 1], then your callback will be run followed by that array.
  • root: Root element is the viewport source to detect visibility. Default value is browser’s viewport. If root value is null or undefined, default value will be set.
  • rootMargin: Margin value of root element. This property value is similar with css margin. e.g. "10px 20px 30px 40px" (top, right, bottom, left). This value can be percentage. This make shrink or increase of each side of bounding box and it is implemented before calculation. Default value is 0.

IntersectionObserver: observer object

let observer = new IntersectionObserver(callback, options);
let target = document.querySelector('#listItem');
observer.observe(target)

...

observer.unobserve(target)
observer.disconnect()
  • observe (function): watch specific HTML element.
  • unobserve (function): stop watching specific HTML element.
  • disconnect (function): observer will stop watching all HTML Element.

Let’s try Intersection Observer in real world

Ex 1. Infinite Scroll

useEffect(() => {
  let observer: IntersectionObserver
  observer = new IntersectionObserver(
      (
          entries: IntersectionObserverEntry[],
          observer: IntersectionObserver
      ) => {
          const [entry] = entries
          if (
              !entry.isIntersecting ||
              articles.length < DEFAULT_PAGINATION_COUNT
          ) {
              return
          }
          loadMore()
          observer.unobserve(
              loadingIndicatorElementRef.current as Element
          )
      },
      {
          threshold: 0.5,
      }
  )

  if (loadingIndicatorElementRef.current) {
      observer.observe(loadingIndicatorElementRef.current as Element)
  }

  return () => {
      if (observer) {
          observer.disconnect()
      }
  }
}, [articles.length, loadMore])

...

return (
  ...
      <div
          className={css`
              display: flex;
              align-items: center;
              justify-content: center;
              width: 100%;
              margin-top: 10px;
              margin-bottom: 10px;
          `}
          ref={loadingIndicatorElementRef}
      >
          {isLoading && <RotatingLines width="100" />}
      </div>
	</ArticleListContainer>
)

So let me explain above code.

We use react’s ref. So loadingIndicatorElementRef will be injected as div element’s ref.

That div element will be HTLM element that will be watched by Intersection Observer.

If use scroll down to the bottom, the specific bottom div element will be visible on the browser.

So the Intersection Observer which watched div element will execute callback function of Intersection Observer. If threshold will be satisfied, entry’s isIntersection property will be changed as true.

Then, that call back function which is same as business logic will be run.

In the above code, callback function will execute loadMore function.

Ex 2. dynamically fixed side bar

const NavItemList = styled.ul<{ isFixed: boolean }>`
    position: ${(p) => (p.isFixed ? 'fixed' : 'relative')};
    top: ${(p) => (p.isFixed ? '1rem' : '0px')};
    width: ${(p) => (p.isFixed ? '230px' : '100%')};

...
`

...

useEffect(() => {
    let observer: IntersectionObserver
    observer = new IntersectionObserver(
        (entries, observer) => {
            const [entry] = entries

            if (entry.isIntersecting) {
                setIsFixed(false)
            } else {
                setIsFixed(true)
            }
        },
        {
            threshold: 0.1,
        }
    )

    if (sideBarTopSpaceRef.current) {
        observer.observe(sideBarTopSpaceRef.current as Element)
    }

    return () => {
        if (observer) {
            observer.disconnect()
        }
    }
}, [])

return (
  <Container style={{ position: 'relative' }}>
      <div
          ref={sideBarTopSpaceRef}
          style={{
              height: '90px',
          }}
      />
      <NavItemList isFixed={isFixed}>
          <NavItem>
              <Link href={`/`} passHref>
                  <NavLink matched={router.pathname === '/'}>
                      <NavLinkText>All</NavLinkText>
                  </NavLink>
              </Link>
          </NavItem>
...

Let me explain this case.

Sidebar should be on fixed position on specific scroll height.

So we will put div element which has specific height (90px), let Intersection Observer watch div element.

After that, if isIntersection === true, isFixedstate will be changed.

← Go home