Blog Intersection Observer – In a Nutshell

PWA | Sep 12, 2022

Intersection Observer – In a Nutshell

Aleksandrs Bogackins

One day, the client might require implementing functionality for the website to do some action when the element is visible. Historically, this required performing calculations triggered by a scroll event, which was generally challenging for developers and can quickly become a performance drawback for your website.

Luckily for us, JavaScript introduced Intersection Observer, which is a better and much more performant way. The JavaScript Intersection Observer API allows for asynchronous checking of the ratio of the intersection of an element with a viewport and will only fire a callback when the predefined thresholds are met.

Images Lazy Loading is probably one of the most game-changing use cases of Intersection Observer among the infinite scrolling or delaying animations until visible.

From MDN Web Docs:

The Intersection Observer API lets code register a callback function that is executed whenever an element they wish to monitor enters or exits another element (or the viewport), or when the amount by which the two intersect changes by a requested amount. This way, sites no longer need to do anything on the main thread to watch for this kind of element intersection, and the browser is free to optimise the management of intersections as it sees fit.

In plain English, Intersection Observer allows detecting when certain elements are visible in our viewport, which only happens when the element meets your desired intersection ratio.

In other words, we have to create an observer that will observe a DOM node and trigger a callback when one or more of the thresholds are met. This threshold can be any ratio from 0 to 1, where 1 means the element is 100% in the viewport and 0 is 100% out of the viewport. The threshold is 0 by default. Here is an example of how to create an observer that I got inspired by from MDN:

Optionally, we can also pass the configuration object as a second parameter to the IntersectionObersver constructor. This object allows us to manage the observer behaviour by configuring 3 possible properties:

  • root: The element that is used as the viewport for checking the target visibility. It must be the target's ancestor. Defaults to the browser viewport if not specified or if null.
  • rootMargin: This set of values serves to grow or shrink each side of the root element's bounding box before computing intersections, the options are similar to those of margin in CSS.
  • threshold: Either a single number or an array of numbers which indicate at what percentage of the target's visibility the observer's callback should be executed, ranges from 0 to 1.0, where 1.0 means every pixel is visible in the viewport.

So the above example could now be adjusted:

Great job! We now have the observer, but it’s not yet observing anything. If we ever want it to start it observing, we need to pass a DOM node to the observe method. Heads-up! The observer can observe any number of nodes, but you can only pass in one at a time. When we no longer want it to observe a node, we call the unobserve method and pass it the node that we would like it to stop watching, or alternatively we can call the disconnect method to stop it from observing any node, like this:

Now let's see an implementation of the intersection observer API using React because we are big boys and girls here, and we want to show our employer who is the boss. We will be writing our own useInView hook, so make sure you are familiar with what React hooks are from our React Hooks Explained article.

First of all, we need to be able to provide the entry that the IntersectionObserver returns from the callback. We can achieve this by utilising the useState hook. Let's make the assumption that we will only observe one node at a time, so we are going to destructure the entries array into the first entry into the array and save that to state.

There is already a big gotcha with this code. Every time the component rerenders, the useInView will be called, which means that the observer is going to be instantiated every time with a new IntersectionObserver. This is not the intended behavior.

The useRef hook can easily adjust this. We use this hook to keep track of the DOM node and do imperative operations with it later (such as giving it focus). We also use the useRef hook to keep values across rerenders. Make sure you know the hook and it's behaviour from our Reach Hooks article. With the Intersection Observer, it is all about the ref itself, which is mutatable and that current value can be reassigned anytime. Still, we will always get back the same ref object with its most recent value on every rerender.

You may now suddenly understand how blurry the difference between useRef and useState hooks is since both will return the current value. Well, in a nutshell, the most significant difference is how you update the value itself and what effect it would have on the rest of the component using it. You can only update the state using the setter function that the useState hook returns, while a new value to the current property can update the ref's value anytime. Also, updating the value of a ref will not signal a rerender, whereas updating the state will.

We are only missing the observing part and need a few things to implement. Let's add a node reference that we will observe with the useEffect hook.

This code, however has a few gotchas. First, the function you return from the useEffect hook runs when the component is unmounting; that way, you can clean up your app by disconnecting the observer, which is exactly what we do. It is generally not safe to access it from the current property as it can, you know, mutate from time to time. In that case, this awesome cleanup functionality may not do the expected job. The safe thing to do here is to assign the current property to a variable in the useEffect hook and then use the variable instead of the current property directly:

Another gotcha is that this code will end in an endless loop because when the observer calls the callback function, it will update the state, which will cause a component to rerender, which in turn will cause the useEffect hook to run again. To fix this, we need to switch from the useRef hook to the useState again:

In this code, we use the callback ref pattern instead of the new ref pattern since we pass the setNode function. This approach will forward the node into the callback we provide and update the state to the new node. Please remember that the node will be null on the first pass, so we must check to ensure the node has some value before observing it.

What do you now think happens if the hook's component changes the node that the observer is 'observing'? What if I tell your this will trigger a state change since the new node will call the setRef function? Since the node has changed, the useEffect will re-run and start observing the new node. So far, so good, right? But how about the old node? You're right; we never stopped observing it, which means that there will be more than one entry in the callback function, and we may save the wrong entry to the state, not to mention that we are observing nodes that we no longer care about. We now should disconnect the observer every time we call the useEffect hook:

This way, we ensure we only observe the node we care about, and all others are omitted. Lastly, we should be able to customize our observer. So, let's pass the configuration object into the hook and provide some default values to enable the safety layer:

Lastly there a just two gotchas left that we have to fix:

  1. The hook does not update the observer if any of the configuration object values change;
  2. useRef will use whatever is passed in as the initial value the first time it’s called, but will ignore it every other render. If it was a simple primitive value, that is no big deal, but we are constructing a new IntersectionObserver object every render, even though it is being ignored on all subsequent renders.

The ultimate fix for two issues here is to move the IntersectionObserver construction into the useEffect hook:

You can now improve your skills and practically wrap your head around by playing with the Intersection Observer within the Codesandbox!

Magebit is a full service eCommerce agency specialized in Magento. At Magebit we create the wonders of eCommerce and support small sites as well as large enterprises.

You can contact us at info@magebit.com or through the contact us page.

Aleksandrs Bogackins
View all articles

Subscribe to our blog

Get fresh content about eCommerce delivered automatically each time we publish.

Other articles