Introduction
There are different reasons to perform some functionalities or animation effects when a user scrolls on a web page. Examples of this could be to toggle a class, to conditionally render different components based on scroll or a certain element position, or to animate elements in and out of the viewport. There are also several ways to get this done; we can use scroll event listeners, scroll animation libraries, or JavaScript's Intersection Observer API.
In this article, we will explore the benefits and drawbacks of these approaches and also go on to discuss further the Intersection Observer API and how it can be used in React.
Prerequisites
Before we proceed, it is important to note that a basic knowledge of HTML, CSS, JS, and React is crucial to fully understanding the concepts discussed in this article. That being said, let's get into it!
Scroll Event Listeners
If you want to make an event happen on Scroll, it is probably a no-brainer to go with a Scroll event listener, as that will give you exactly what you want... initially. But as I'm sure there will be other edge cases to consider in a real-life project, you'll soon realize that this brings even more trouble because this said event would then happen every time the user scrolls on the webpage. Unless in specific use cases, this will ultimately become annoying to the user and eventually lead to some issues, such as;
Performance: Scroll event listeners trigger an event every time the user scrolls the page, which can lead to a high number of events being fired. Handling these events in real-time can be computationally expensive and cause performance issues, especially on complex web pages or on devices with limited resources.
Jank and Stuttering: When scroll event listeners are used to update elements on the page, such as animating or manipulating DOM elements, it can result in janky and stuttering scrolling experiences. This occurs because the scroll events are processed in the main thread, potentially blocking other critical operations.
Lack of Control: Scroll events fire continuously while scrolling, even if you're only interested in specific points or sections on the page. This can make it challenging to optimize code or trigger actions at specific scroll positions.
Scroll Animation Libraries
Animation is one of the most common usages of scroll events. Having elements fade in and fade out, changing shapes and styles as the user scrolls through the webpage, has been a game changer in designing aesthetically pleasing websites.
My personal top two animation libraries for React are Framer Motion and GSAP. These libraries are hands down the best out there right now, in my opinion, and are more than capable of bringing wild creative imaginations to life.
But most times, it is just best to really keep simple things simple. Sure using animation libraries may be fancy and whatnot, but if what you're trying to do is straightforward and simple enough, I see no reason to bloat up your code base with another library when you can achieve the same thing with a built-in Javascript API.
Intersection Observer API
While working on an open-source project I'm contributing to over the past week, there was a need to implement the design of a component that changes state based on its position in the viewport. I knew that I didn't want the state change to happen just on scroll, and I also did not want to install any third-party package unless it was absolutely necessary. After much research and trial, Intersection Observer API gave me the perfect solution I was looking for, which was the main motivation behind this article.
The IntersectionObserver()
is a JavaScript API designed specifically for efficiently observing changes in the intersection of an element with its parent container or the viewport. Apart from the already mentioned need to keep things simple, it also offers the following improvements over scroll event listeners:
Performance Optimization: Intersection Observer is designed to optimize performance by providing a callback that executes when elements enter or exit the viewport or another defined area. This approach reduces the number of events fired and allows the browser to handle the observations more efficiently.
Asynchronous Execution: The scrolling and layout operations are separated from the JavaScript execution via the Intersection Observer API's asynchronous operation. This division eliminates jank and stuttering, resulting in a more fluid scrolling experience.
Granular Control: You can provide particular targets and thresholds for observing element visibility with Intersection Observer's second parameter, which is an
options
object. This gives you more control and flexibility by allowing you to selectively watch and respond to changes in particular parts or sections of a page.Better Resource Management: Intersection Observer helps to optimize the use of system resources by keeping track of element visibility. It enables you to save bandwidth and enhance efficiency by allowing you to load or unload content, lazy load images, or activate animations only when necessary.
Enough talk. Show me the code!
Now we are going to build a simple project just to demonstrate how the Intersection Observer API works and how we can use it within a React project. The aim of this project is simply to change the styles of the navigation bar and also make it stick to the top of the page when the user scrolls.
We start by bootstrapping a React project with Vite by navigating to a preferred directory and running the following commands:
npm create vite@latest
// select Typescript as the preferred language, and
// specify the project name, here it is react-intersection-observer
cd react-intersection-observer
npm install
npm run dev
After running these commands, we should have our React project running in the browser
Go ahead and delete index.css
file as we won't be needing it and in App.css
, add the following code:
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
#root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
text-align: center;
}
.sentinel {
background-color: skyblue;
height: 5px;
}
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
background-color: skyblue;
padding: 2rem;
}
.stuck {
position: sticky;
top: 0px;
box-shadow: rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, rgba(0, 0, 0, 0.3) 0px 30px 60px -30px;
color: #fff;
transition: all 0.3s;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
}
section {
width: 90%;
box-shadow: rgba(50, 50, 93, 0.25) 0px 6px 12px -2px, rgba(0, 0, 0, 0.3) 0px 3px 7px -3px;
border-radius: 10px;
padding: 50px;
margin: 50px 0;
}
.title {
font-size: 2rem;
margin-bottom: 2rem;
}
.content {
color: #999ea9;
line-height: 2rem;
text-align: justify;
}
footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 2rem;
color: #fff;
background-color: skyblue;
}
@media screen and (min-width: 768px) {
section {
width: 60%;
}
}
The above style declarations contain all the styling required for the finished project. Nothing fancy going on, just basic CSS to make everything look clean.
In main.tsx
, remove the import for index.css
and leave everything else as is.
Now, in the App.tsx
, this is where everything comes together. Go ahead and replace everything with the following code:
import { useEffect, useRef, useState } from 'react';
import './App.css';
function App() {
const [intersecting, setIntersecting] = useState<boolean>(true);
const sentinelRef = useRef<HTMLDivElement | null>(null);
const navRef = useRef<HTMLDivElement | null>(null);
const observerOptions = {
root: null,
rootMargin: '0px',
treshold: 1.0
};
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (!entry.isIntersecting) {
setIntersecting(true);
navRef.current?.classList.add('stuck');
} else {
setIntersecting(false);
navRef.current?.classList.remove('stuck');
}
}, observerOptions);
if (sentinelRef.current !== null) {
observer.observe(sentinelRef.current);
}
});
return (
<main>
<div className="sentinel" ref={sentinelRef}></div>
<nav ref={navRef} className="navbar">
<h2>Hashnode Articles</h2>
<h3>Emmanuel Oloke</h3>
</nav>
<div className="container">
<section>
<h1 className="title">Using Intersection Observer API in React</h1>
<article className="content">
There are different reasons to perform some functionalities or animation effects when a
user scrolls on a web page. Examples of this could be to toggle a class, to
conditionally render different components based on scroll or a certain element position,
or to animate elements in and out of the viewport. There are also various ways to get
these done; we can use scroll event listeners, scroll animation libraries, or
Javascript's IntersectionObserver API. In this article, we will explore the benefits and
drawbacks of these approaches and also go on to discuss further the IntersectionObserver
API and how it can be used in React...
Building Charts with React and ChartJS
Data visualization tools are powerful for analyzing and communicating complex data sets
in a more accessible and intuitive way. With the advent of modern web technologies,
creating interactive data visualizations has become easier than ever before. React and
Chart.js are two popular technologies developers can use to create dynamic and
interactive data visualizations...
useEffect vs useSWR
The concept of data fetching in React applications is of high importance as it is often
necessary to fetch data from an external source, such as an API or a database, and use
that data to render components. React provides several ways to fetch data, including the
built-in fetch method and popular third-party library Axios. One popular approach to
data fetching in React is to use hooks like useEffect and useSWR from the swr
third-party npm package. These hooks allow developers to fetch data and manage its state
within a component, making it easy to update the UI in response to changes in the
data...