Briefly looking at IntersectionObserver
#mdn
#browser api
#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.
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
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
, isFixed
state will be changed.