Intersection Observer

Date published: 15-Apr-2020
Date modified: 16-Apr-2020
2 min read / 406 words
Author: Paul Scanlon

React
JavaScript

Recently at work i was asked to create a scroll-jacking sticky nav and after trying out numerous open source projects react-visibility-sensor, use-scroll-position and react-scrollmagic to name a few i settled on rolling my own using React, vanilla JavaScript and the Intersection Observer API

In this post i'll show you how to create a reusable Intersection Observer component with React.

The IntersectionObserver component will manage a few things. These are;

  • Creating a new IntersectionObserver instance
  • Creating a DOM ref
  • Rendering it's children.

A first pass at this component looks like this 👇


// ReactObserver.js
import React, { useRef, useEffect } from "react"
const ReactObserver = ({ children }) => {
const ref = useRef(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
console.log("ReactObserver is more that 0.2 percent visible")
}
},
{
root: null,
rootMargin: "0px",
threshold: 0.2,
}
)
if (ref && ref.current) {
observer.observe(ref.current)
}
}, [])
return <div ref={ref}>{children}</div>
}
export default ReactObserver

And to use it you can do something like this 👇


// SomeComponent.js
import ReactObserver from "./RectObserver"
...
<div style={{ height: "100vh", background: "red" }} />
<ReactObserver>
<h1>Oh hai</h1>
<p>I'm a child of the React Observer</p>
<img
src="http://place-puppy.com/2000x2000"
alt="puppy"
style={{ width: 300, height: 300 }}
/>
</ReactObserver>
...

This isn't going to do very much but if you've got this far you'll see a console.log() when the ReactObserver component enters the viewport.

You may have also noticed there's a <div /> with a height of 100vh which will push the image out of the viewport. At the moment the browser is still going to try and download the image even though it can't be seen by the user... this isn't great ☝️

Let's correct that!

For the second pass at this component we're going add a little bit of state to the ReactObserver component using React hooks and instead of just logging a message to the console we're going to change the state.

This state change will allow us to return or not return the children.

In practical terms this means that the <img /> tag won't be in the DOM and the browser won't attempt to download it until the ReactObserver component enters the viewport.

When the ReactObserver component enters the viewport we can set the isChildVisible state hook to true which when combined with a ternary operator can be used to determine if we should render the children.


// ReactObserver.js
import React, { useRef, useEffect, useState } from "react"
const ReactObserver = ({ children }) => {
const ref = useRef(null)
const [isChildVisible, setIsChildVisible] = useState(false)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsChildVisible(true)
}
},
{
root: null,
rootMargin: "0px",
threshold: 0.2,
}
)
if (ref && ref.current) {
observer.observe(ref.current)
}
}, [isChildVisible])
return <div ref={ref}>{isChildVisible ? children : null}</div>
}
export default ReactObserver

There are more methods besides entry.isIntersecting which i've found to be quite useful.

For instance using entry.intersectionRatio is useful for determining when the component enters and exits the viewpoint, and by changing the threshold values and adding conditions you can determine when certain events are fired.


import React, { useRef, useEffect, useState } from "react"
const ReactObserver = ({ children }) => {
const ref = useRef(null)
const [isChildVisible, setIsChildVisible] = useState(false)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsChildVisible(true)
}
if (entry.intersectionRatio > 0) {
console.log("ReactObserver has entered the viewpoint")
}
if (entry.intersectionRatio <= 0) {
console.log("ReactObserver has exited the viewpoint")
}
},
{
root: null,
rootMargin: "0px",
threshold: [0, 1],
}
)
if (ref && ref.current) {
observer.observe(ref.current)
}
}, [isChildVisible])
return <div ref={ref}>{isChildVisible ? children : null}</div>
}
export default ReactObserver

To make your ReactObserver component even more reusable you might want to consider exposing some of the internal configuration via props


import React, { useRef, useEffect, useState } from "react"
import PropTypes from "prop-types"
const ReactObserver = ({ children, threshold }) => {
const ref = useRef(null)
const [isChildVisible, setIsChildVisible] = useState(false)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsChildVisible(true)
}
if (entry.intersectionRatio > 0) {
console.log("ReactObserver has entered the viewpoint")
}
if (entry.intersectionRatio <= 0) {
console.log("ReactObserver has exited the viewpoint")
}
},
{
root: null,
rootMargin: "0px",
threshold: threshold,
}
)
if (ref && ref.current) {
observer.observe(ref.current)
}
}, [isChildVisible, threshold])
return <div ref={ref}>{isChildVisible ? children : null}</div>
}
ReactObserver.propTypes = {
/** The threshold values */
threshold: PropTypes.oneOfType([
PropTypes.number,
PropTypes.arrayOf(PropTypes.number),
]),
}
export default ReactObserver

And to use it you can do something like this 👇


<ReactObserver threshold={0.5}>
<h1>Oh hai</h1>
<p>I'm a child of the React Observer</p>
<img
src="http://place-puppy.com/2000x2000"
alt="puppy"
style={{ width: 300, height: 300 }}
/>
</ReactObserver>

In the above example the threshold is set to 0.5 which translates to the IntersectionObserver not firing until at least 0.5 of the component has entered the viewport

There are way more scenarios when using an IntersectionObserver will be helpful in your project but i hope this at least helps you understand the basic concept.

Happy observing 😊