Sitemap

Using IntersectionObserver With Vue.js

5 min readJul 12, 2020
Press enter or click to view image in full size
IntersectionObserver example

A while ago I wanted to try the IntersectionObserver API within a Vue.js app that I've been working in. Since this was my first time using it and I didn't really understand how it works, I searched for some implementation examples on Vue, but couldn't really find what I needed. So here's a little guide that I hope it's useful for you to understand the basics and implement it on your Vue apps.

According to the MDN docs:

“The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport.”

There are lots of neat use cases for this API, such as image lazy loading and infinite scrolls; but I’ll be focusing on explaining the one that I needed: to know when an element was visible within its scrollable container.

You can see the final result in Codepen:

A Basic Example

We'll start with a basic example to cover the basics, this is just a simplified version of the examples we can find at the docs. Let's take a look at how an IntersectionObserver is created:

You can see that we have a very simple app markup: one observer-root element, which could be the scrollable container and multiple observable elements; these are the ones that we wish to know if they've been scrolled in and are visible for our users.

We've also created the IntersectionObserver with a root property within its constructor arguments and we observe all the elements with the observable class name; therefore, whenever these elements are visible within the root, the callback will be invoked. If the root option is omitted, it will default to the document's viewport, which could cause the callback to be invoked unexpectedly.

You can test this in Codepen. I've added some styles to make the root scrollable and to add an effect to the observable elements when they are in sight:

Working With Vue.js

In a Vue.js app, we find more complex scenarios than the previous example. We'll usually have a parent component with the root element and the observable elements deeper in the component hierarchy. Another obstacle we can find is that accessing DOM elements with id, classes or query selectors is not reliable, given the way Vue renders the components DOM sub trees.

Vue.js refs allows us to deal with those issues in most cases. However, for IntersectionObserver implementations, it's a bit more complicated because:

$refs are only populated after the component has been rendered, and they are not reactive. It is only meant as an escape hatch for direct child manipulation - you should avoid accessing $refs from within templates or computed properties.

Meaning that if we try to observe elements using refs, we can't always assure they contain existing DOM element references if the children components are conditionally rendered with v-if or v-show directives, rendered as part of a list with v-for, lazy loaded or even if they have the appropriate height or width that causes them to overflow their container boundaries.

The easiest way I found is to simply define the IntersectionObserver in the parent component and delegate the responsibility of observing the elements to its children, because they have direct reference to the DOM elements and more accurate knowledge when they are ready to be observed.

For example: we could take advantage of the children components mounted hooks, where their DOM tree has been rendered, also we could set up watchers for reactive properties that allow conditional rendering and even use $nextTick to ensure the elements exist.

IntersectionObserver example with Vue.js

We'll be building a to-do list. Whenever a to-do item is scrolled in and is fully visible within the list container, it'll be marked as seen. We'll be using an IntersectionObserver that will invoke the callback that modifies the seen flag for the to-do item that entered the view, finally, we'll make the observer to stop watching the element that has already marked as seen, because we don't want the browser to continue computing the intersection for that element.

We start by creating two components: Todo and TodoList. For now, they're just in charge of rendering the UI for the list and its items.

Now we'll be creating the IntersectionObserver instance in TodoList component:

Let's break down the changes:

  1. We added a reference to the observer in the component's data. This is important since we'll be passing the instance as a property to its children components.
  2. We added a created hook, where we initialized the observer. For this step, we could either use created or mounted hooks, just make sure the root element is available. Also, notice that we use this.$el as the observer's root. This could also be replaced for a ref to TodoList DOM element.
  3. Finally, we added the onElementObserved method. It's currently empty, but it will later be in charge of the logic when a Todo is scrolled in.

Now, we'll handle when the Todo items are scrolled in with simple changes:

  1. We added observer and index properties to the Todo component. The former is the reference to the IntersectionObserver instance that we'll use to handle when the item is scrolled in. The index is a value that we'll use as a custom data-index attribute that we can extract in the handler to identify the item that entered the view.
  2. We used the Todo component's mounted hook to observe the item. Similar to how we initialized the observer in TodoList, we could've used here a ref to another element, or a reactive property watcher and $nextTick to assure the DOM element exists. Since this is a very simple scenario, we omitted that, but consider using that approach if you need to fetch information, lazy load components or conditionally render the target elements.
  3. We added the handler's logic. The isIntersecting value tells us when the element is actually visible. If it does, then we stop observing the element so we don't trigger the handler for this element anymore. Finally, we change the seen flag 1 second after the item has entered the scrolled in area. The custom data-index attribute is extracted from the target DOM element, which is the only reference to the item that entered the scroll.
  4. To ensure the browser is not computing the intersection, we added the beforeDestroy hook, where the observer is disconnected, causing it to stop observing all the registered elements.

This turned to be an interesting approach, since it can be adapted to other use cases that are way more complex than a to-do list. I've included some tips to deal with Vue refs when the content is rendered asynchronously or is not always available. Just have in mind that most template directives could require extra steps to assure the elements can be properly observed.

You can see the final result in Codepen. I added some extra pieces of code just to better illustrate the functionality, feel free to play with it and try it for yourselves.

References

--

--

Sebastián Segura
Sebastián Segura

Written by Sebastián Segura

Developer | Half human | Half sloth | Half addicted to coffee

Responses (3)