My experience with IntersectionObserver, scroll snap and React
Tags: javascript, react, css
Published: 2020-05-02 11:00:36


It fascinates me how powerful but also how fragile JavaScript is. Until around 2010, no one would ever describe himself as a JS developer. Now-a-days thanks to node, the game changed and JS is one of the most used languages with every new iteration from TC39 making it stronger. To make it even more attractive, I think it's the only language that contains so many libraries/frameworks to work with it. Angular, Vue, React, Svelte, good old Jquery, Ember, Meteor, Stimulus, and so on... It's crazy.

And what do we do? We work with one of these libraries that makes our work easier and more productive, right? We became experts in that framework but forgot the initial abstraction, how Vanilla JS is handling everything behind the scenes. Today it's easier than ever to just download packages that do precisely what we are looking for, but we are not able to understand the non fancy code, the one coming from Vanilla JS.

And here comes the motivation behind my post: I want to talk about the *Intersection Observer* API.  Going first through some pure JS examples, and how I later moved this code into a react environment. My intention is to explain my experience rather than have perfect code, so you might find some code that can be refactored along the way. Feel free to DM me [@RubenSuet](https://twitter.com/RubenSuet) and I can double-check it 😊.

## The use case
To understand why I need to play with some CSS rules or JS API, I need to explain what my issue was and how I started to tackle it. Currently I am a F.E dev in an E-commerce in the EU and like all of the e-shops in the world, the homepage consists mainly of getting the attention of the user, showing some products (maybe personalized, maybe not), and adding hot deals to make even more attractive the idea of buying some product. 

And within all these deals and promotions, how can I display a big amount of products without overwhelming the user? With **Carousels**. They are ugly and are low performant but the user has control. If he wants to see more products he can. We are not forcing him to see all of them, just some of them, and it's up to them to keep clicking and interacting with the carousel to see more. On top of that, we have server-side rendering which means that the only way to make proper responsive pages are with pure CSS or guessing some User-Agent (this last one is pretty dangerous because you can never know which resolution is set). 

And to top it all of, on mobile, there's one carousel that doesn't look at all like the one for Desktop. At this point you're guessing right: We are rendering multiple carousels for specific devices ( *Sigh*, I know. We are working on improving this which is also the reason why I am writing here: to share my progress). So, what can I do to perform better? 

## Some research
Let's check some E-commerce/marketplaces to see how they handle it. I did some research on several websites, but I would like to highlight 2 specific websites: Amazon and Zalando. Both have the same use case. You enter the website and they have something that looks like a carousel to show the products/categories. But it's not.
![Amazon research Intersection Observer](https://www.rubensuet.com/static/70258a78a7d38b7c40591768f6ae878f/f6df3/amazon.jpg)

![Zaladon research Intersection Observer](https://www.rubensuet.com/static/3a39a9766cd2bf00e66b36337239396a/8109b/zalando.jpg)


Notice how they are working with a simple list and are achieving a perfect "Carousel". The user is not even noticing it and the performance is just great. So how do they do it? Checking a bit the code, in the ul element I found the following:
```css
element.style {
1.  scroll-padding-left: 672px;
2.  scroll-behavior: smooth;
3.  scroll-snap-type: x mandatory;
}
```
&nbsp;

AHA! I had heard about this but never needed to work with it. This is what is called snap-scroll. It lets you create the effect of scrolling where you can position the scroll in specific alignment from the item, making the effect of the carousel. Please check as a reference [this article from Max Kohler](https://css-tricks.com/practical-css-scroll-snapping/) and [this other article from Robert Flack and Majid Valipour](https://developers.google.com/web/updates/2018/07/css-scroll-snap). 

So I presented this articule to my team seeing if we could try to do something like the examples shown above. And then... my dreams were gone. The analytics person from my team explained to me that it's not possible to track which elements are visible at specific moments and to be honest, I hated that idea, but he had a point.

How can I make it possible... Before continuing I decided to procrastinate for a bit on twitter when suddently I saw that [@meduzen had posted exactly what I was looking for](https://twitter.com/meduzen/status/1230242061124800513?s=20). He played with the *Intersection observer* to make sure that when an element is visible, it toggles a class and does a CSS animation. That's perfect. If I can toggle a class, I can for sure trigger a callback and make the analytics work, can't I? 

More research on the internet showed me how to use the Intersection Observer (I'll post all my references at the end of this post), but none of them were tackling it in React. The only thing I found was an [npm package](https://www.npmjs.com/package/react-intersection-observer), but this was exactly the intro of my post and before I use a solution that is already built, I wanted to understand the real abstraction and how it works. So I wanted to do it by myself, without dependencies to other libraries that do all the magic without you knowing what is going on.

## Building my component with scroll-snap and Intersection Observer

First I'll show what I built and then I'll break it into smaller pieces. Here's a picture showing what I want to accomplish:

![Goal for my Post](https://www.rubensuet.com/e0d01629e934da58c551efdf64b212ab/goal.gif)

So, I have some sections, and when I am scrolling, I want to console log in which section I am at that specific moment.  Here is my pure react component to achieve it:

```javascript
//CarouselScroller.tsx
import  *  as  React  from  "react";
import  {  Section,  LightSection,  Container  }  from  "./CarouselScroller.styled";
const  CarouselScroller:  React.FC<{}>  =  ()  =>  {
	return  (
	<Container>
		<Section  color="#134611">
			Section 1
		</Section>
		<Section color="#3E8914">
			Section 2
		</Section>
		<Section color="#3DA35D">
			Section 3
		</Section>
		<LightSection color="#96E072">
			Section 4
		</LightSection>
		<LightSection color="#E8FCCF">
			Section 5
		</LightSection>
	</Container>
	<button  onClick={() =>  setCount(count + 1)}> Re-render</button>
	);
};

CarouselScroller.displayName  =  "CarouselScroller";
export  default  CarouselScroller;

```
&nbsp;

I used [styled components](https://styled-components.com/) and made the `<Container>` the `<Section>` and the `<LightSection>`

```javascript
// CarouselScrollect.tyled.ts
import  styled  from  "styled-components";

const  Container  =  styled.div`
`;

const  Section  =  styled.div<{ color:  string  }>`
	background:  ${props  =>  props.color};
	min-width:  70vw;
	height:  30vh;
	color:  white;
	display:  flex;
	align-items:  center;
	justify-content:  center;
	font-size:  28px;
`;

const  LightSection  =  styled(Section)`
	color:  #1f2d3d;
`;

export  {  Container,  Section,  LightSection  };
```
&nbsp;

With these 2 files. I got exactly what you saw in the previous gif. However, it is still lacking the `scroll snap`.  Let's add it
```javascript
// CarouselScrollect.tyled.ts
import  styled  from  "styled-components";

const  Container  =  styled.div`
	scroll-snap-type:  x  proximity;
	display:  flex;
	overflow-x:  scroll;
`;

const  Section  =  styled.div<{ color:  string  }>`
	scroll-snap-align:  center;
// Other properties non related with scroll-snap
`;

```
&nbsp;

 - scroll-snap-type: You need to specify how it locks into the viewport when it scrolls. That usually is the parent component that wraps the children to make the effect and is the first rule you need to specify to use the snap module. Usually, you can specify the axis `x` or `y` and choose as a second option which kind of 'lock' you want to use. There are 3 properties:
	 - `none`: You scroll normally, it doesn't force the scroll to anchor specifically at some point of the element
	 - `proximity`: When scrolling, between 2 elements it can force to anchor into one specific element. 
	 - `mandatory`: The most strict option. It forces always to anchor the element where you align (is the option that the children have. We'll discuss it in a moment. I like this option if you want to do the effect of parallax or close it as a carousel.
 - Scroll-snap-align: Where do you want to align the item when it locks the scroller. The image from [Robert Flack and Majid Valipour](https://developers.google.com/web/updates/2018/07/css-scroll-snap) explains well this concept. I upload it here, but please remember this picture belongs to them ( and therefore they deserve to be referenced)

![Alignment between elements with scroll snap](https://www.rubensuet.com/static/292c3d089d7c16e6264f90b9ed92bd41/e8a7f/alignment.jpg)

I will provide a sandbox link with the working project, feel free to do some playgrounds with the options. In the end, a picture is worth a thousand words.

Time to play with *Intersection observer*.  Some Vanilla JS to make it work before we go. This is how it looks:

```javascript
let observer = new IntersectionObserver(callback, options)
```
&nbsp;

Check out we need a `callback` and `options`. Let's start with the last one since it's the easier one: 
```javascript
let options = {
	root: null // relative to document viewport
	rootMargin: '0px'// margin around root. Values are similar to CSS property. Unitless values not allowed
	threshold: 1.0 // visible amount of item shown concerning root
}
```
&nbsp;

I tried to explain in the code itself what every option does but mainly you can specify another root ( like `#gallery`), the `rootMargin` if it needs a margin to start with and last (and I would say the most important one) but not least the `threshold`. How much of the item needs to be shown to trigger your callback with values from 0 (hidden) to 1 (fully shown). In my case it needs to be fully visible.

Let's check the callback
``` javascript
let callback = (entries, observer) {
	for(let entry of entries) {
		if (entry.intersectionRatio  >=  1) {
			console.log('I am visible!')
		}
	}
}
```
Please, note this is a simplified version just for learning purposes. You can fulfill it with any logic you want. 

 - `entries` are going to be an array of `IntersectionObserverEntry`(thanks TS to help to put a name on this). And that's when I iterate it, you have the property `intersectionRatio` which is the one that determines if it's visible or not ( again, the value goes from 0 to 1).

Connecting the options and the callback you can notice that we specified a threshold of 1 and we check in the callback if this true, and if it is, then we do log it.

If you want to know more about `IntersectionObserver` check out the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). Apart from triggers for when elements are visible, IntersectionObserver lets you do lazy loading, infinite scrolling fetching new data, between others. Mainly it reacts when an element is in focus of the user. Cool API to be honest and I am not sure how I went so far without having the necessity to work with it.

At the end you can observer elements like
```javascript
const images = document.querySelector("img")
for(let image of images) {
	observer.observe(image)
}
```
&nbsp;

In this example, the observer reacts for all images in your document and does whatever you need to do.

Going back into the react component. Let's make it step by step:
```javascript
const  CarouselScroller:  React.FC<{}>  =  ()  =>  {
	const  refs  =  React.useRef<HTMLDivElement[]>([]);
	const  observer  =  React.useRef<IntersectionObserver>(null);
	const  addNode  = (node:  HTMLDivElement)  =>  refs.current.push(node);

	React.useEffect(()  =>  {
		if  (observer.current)  observer.current.disconnect();
				
		observer.current  =  new  IntersectionObserver(handler,  options);
		for  (const  node  of  refs.current)  {
			observer.current.observe(node);
		}
		return  ()  =>  observer.current.disconnect();
	},  []);
	  
	return  (
		<Container>
			<Section  ref={addNode} color="#134611">
				Section 1
			</Section>
			<Section  ref={addNode} color="#3E8914">
				Section 2
			</Section>
			<Section  ref={addNode} color="#3DA35D">
				Section 3
			</Section>
			<LightSection  ref={addNode} color="#96E072">
				Section 4
			</LightSection>
			<LightSection  ref={addNode} color="#E8FCCF">
				Section 5
			</LightSection>
			</Container>
	);
};
```

&nbsp;

Step by step:
```javascript
	const  refs  =  React.useRef<HTMLDivElement[]>([]);
	const  observer  =  React.useRef<IntersectionObserver>(null);
	const  addNode  = (node:  HTMLDivElement)  =>  refs.current.push(node);
```
&nbsp;

Notice that I made it in TS (If you feel uncomfortable, just remove the `<>`). So first I create an array of refs. Here I want to store the html elements to observe afterwards. Then I create another ref for the observer. Without refs, a new IntersectionObserver would be created for every re-render, and I don't want that. At the end a quick method to push the refs into the array I declared before. And this is how I store them in the `return` method:
```javascript
<Section  ref={addNode} color="#134611">
	Section 1
</Section>
```

&nbsp;

So with that, I can have all my refs stored. Now let's check my `useEffect`. 
```javascript
React.useEffect(()  =>  {
	if  (observer.current)  observer.current.disconnect();
				
	observer.current  =  new  IntersectionObserver(handler,  options);
	for  (const  node  of  refs.current)  {
		observer.current.observe(node);
	}
	return  ()  =>  observer.current.disconnect();
},  []);
```

&nbsp;

It's important to wrap it in a useEffect to make sure it will only be rendered **JUST** after the component is mounted. Otherwise, you won't have the refs. The first thing I do inside is to check if I have already an observer. In the case of true, then I use the `disconnect` method. This method lets me 'unobserve' all elements we were observing. So this is a kind of 'reset' to start again and observe again, in case we already had an observer.

Afterward, we create the observer with a handler and options, and we iterate all those `refs` to be observed. Notice that I return a method to make sure that I `disconnect` as well when this component is unmounted.  If you are confused returning a method inside a use effect, [check this article from React docs](https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup).

Let me show you my `handler` and `options`: 
``` javascript
const  handler  =  (
	entries:  IntersectionObserverEntry[],
	observer:  IntersectionObserver
)  =>  {

	for  (const  entry  of  entries)  {
		if  (entry.intersectionRatio  >=  1)  {
			console.log("i Am visible",  entry.target.textContent);
		}
	}
};

const  options  =  {
	root:  null,
	rootMargin:  "0px",
	threshold:  1.0
};
```

&nbsp;

And with that.... **MAGIC**, we got it! Together, dear reader, we achieved the goal I set at the beginning of this section! 

But wait... there's a couple of gotchas and refactoring to do. Checking the react docs we find the following [FAQ](https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily). It seems our intersection observer is creating it every time we re-render, and we don't want that. So we can refactor it to create the ref as a lazy load. The following snippet is just with the necessary changes to achieve it:
```javascript
const  getObserver  =  (ref:  React.MutableRefObject<IntersectionObserver  |  null>)  =>  {
	let  observer  =  ref.current;
	if  (observer  !==  null)  {
		return  observer;
	}
	let  newObserver  =  new  IntersectionObserver(handler,  options);
	ref.current  =  newObserver;
	return  newObserver;
};

const  CarouselScroller:  React.FC<{}>  =  ()  =>  {
	const  observer  =  React.useRef<IntersectionObserver>(null);
	React.useEffect(()  =>  {
		if  (observer.current)  observer.current.disconnect();
		const  newObserver  =  getObserver(observer);
		
		for  (const  node  of  refs.current)  {
			newObserver.observe(node);
		}
		return  ()  =>  newObserver.disconnect();
	},  []);
	return (...)
}
```

&nbsp;

I presented `const  observer  =  React.useRef<IntersectionObserver>(null);` but when I was doing some playgrounds, I did `const observer = React.useRef<IntersectionObserver>(new IntersectionObserver(handler,options));`. That it's causing a new object every render, and therefore some performance errors. 

Another gotcha well pointed coming from [@aytee17](https://twitter.com/aytee17/status/1231608326649049089?s=20) is that, for every render, we'll call the `ref` callback in the return method, and it will start to increase dramatically my array of references. The `ref callback` is being triggered twice: once when it's mounted into the DOM, and another when it's being removed from the DOM( it calls the callback, but the ref holds as `null` value instead of the HTML element).  In short words: My first render my array will have 5 elements ( 5 sections that I add in this example), if I force a re-render, I will have 15 elements: 

 - 5 HTML elements from my first render and added into the DOM
 - 5 nulls from when the elements were removed from the DOM
 - 5 elements from the new re-render that are added into the DOM

So, my proposal, it's to wrap the `addNode` method into a `useCallback`. If you are wondering what it does, [here I tried to explain it with my own words](https://github.com/kentcdodds/ama/issues/718#issuecomment-515358619). 

My final result:
```javascript
import  *  as  React  from  "react";
import  {  Section,  LightSection,  Container  }  from  "./App.styled";

const  handler  =  (
	entries:  IntersectionObserverEntry[],
	observer:  IntersectionObserver
)  =>  {
	for  (const  entry  of  entries)  {
		if  (entry.intersectionRatio  >=  1)  {
			console.log("i Am visible",  entry.target.textContent);
		}
	}
};

const  options  =  {
	root:  null,
	rootMargin:  "0px",
	threshold:  1.0
};

const  getObserver  =  (ref:  React.MutableRefObject<IntersectionObserver  |  null>)  =>  {
	let  observer  =  ref.current;
	if  (observer  !==  null)  {
		return  observer;
	}
	let  newObserver  =  new  IntersectionObserver(handler,  options);
	ref.current  =  newObserver;
	return  newObserver;
};

const  CarouselScroller:  React.FC<{}>  =  ()  =>  {
	const  [count, setCount] =  React.useState(0);
	const  refs  =  React.useRef<HTMLDivElement[]>([]);
	const  observer  =  React.useRef<IntersectionObserver>(null);
	const  addNode  =  React.useCallback(
		(node:  HTMLDivElement)  =>  refs.current.push(node)
	,[]);
	// ref callback is called twice: once when the DOM
	//node is created, and once (with null) when the DOM
	//node is removed.
	// TRY IT OUT => Comment the other addNode and uncomment this one
	//const addNode = (node: HTMLDivElement) => refs.current.push(node);

	React.useEffect(()  =>  {
		if  (observer.current)  observer.current.disconnect();
		const  newObserver  =  getObserver(observer);
		
		for  (const  node  of  refs.current)  {
			newObserver.observe(node);
		}
		return  ()  =>  newObserver.disconnect();
	},  []);

	console.log("render",  refs);
	return  (
		<React.Fragment>
			<Container>
				<Section  ref={addNode} color="#134611">
					Section 1
				</Section>
				<Section  ref={addNode} color="#3E8914">
					Section 2
				</Section>
				<Section  ref={addNode} color="#3DA35D">
					Section 3
				</Section>
				<LightSection  ref={addNode} color="#96E072">
					Section 4
				</LightSection>
				<LightSection  ref={addNode} color="#E8FCCF">
					Section 5
				</LightSection>
			</Container>
			<button  onClick={() =>  setCount(count + 1)}> Re-render</button>
		</React.Fragment>
	);
};

  

CarouselScroller.displayName  =  "CarouselScroller";

export  default  CarouselScroller;
```
&nbsp;


I added a `useState` to force a re-render. I added as well comment and a proposal, so you can see by yourself the problem to not wrap the method with `useCallback`

### [Check the code working in Sandbox](https://codesandbox.io/s/intersectionobserver-react-6scld)

It's been an exciting journey to feel more confident with `IntersectionObserver`, and to document all my steps and how it helped me be more confident with it. My next steps are to generate these effects with real products and start and apply them to my job. 

Code is just code, you won't harm anyone. So don't be afraid to go and understand the abstraction. If something is not clear, don't be afraid to ask the internet how to tackle it, and I encourage you to document it and explain it as well. 

### References for this post
[Practical CSS SCroll Snapping](https://css-tricks.com/practical-css-scroll-snapping/)

[Well-Controlled Scrolling with CSS Scroll Snap](https://developers.google.com/web/updates/2018/07/css-scroll-snap)

[How to do scroll-linked animations the right way](https://blog.logrocket.com/how-to-do-scroll-linked-animations/)

[Creating a RevealJS clone with CSS Scroll Snap Points](https://medium.com/@Nico3333fr/creating-a-light-revealjs-clone-with-css-scroll-snap-points-306dfba71652)

[Intersection Observer: Track elements Scrolling  Into View](https://webdesign.tutsplus.com/tutorials/how-to-intersection-observer--cms-30250)

[How To Use an IntersectionObserver in a React Hook](https://medium.com/the-non-traditional-developer/how-to-use-an-intersectionobserver-in-a-react-hook-9fb061ac6cb5)

See the original post at my blog [suetBabySuet](https://www.rubensuet.com/intersectionObserver/)