Intersection Observer
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 😊