Intersection Observer APIの実用的な使い方とたまに罠

Intersection Observer APIは、After IE元年（A.I.0）の今年から安心して使える便利機能です。
ただ歴戦のJS書きに取っては知られていなかったり、知っていても調子に乗って使うと罠にハマったりすることもある機能なので、今回は便利さと、こんなところには注意しようみたいな話を書きます。

# 基本的な使い方

下記のCodePenの破線エリアをスクロールしてもらうと、赤い矩形が破線エリア内にある場合に、破線エリア上部のfalseがtrueに変わるというものです。
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="YzvgROB" data-user="kaitou1192" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/kaitou1192/pen/YzvgROB">
  Untitled</a> by Kaitou (<a href="https://codepen.io/kaitou1192">@kaitou1192</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>

これは、いにしえのJSだとスクロール量と破線の領域のサイズを確認しつつ、中の赤い矩形のサイズと照らし合わせるという書き方になると思います。この際 `load` `scroll` `resize` のを監視しなければならないため、ぱっと思うだけでも **「間引かないと死ぬ……。」** みたいな心境になるはずです。

しかしIntersection Observer APIでは、ちょっとまわりくどい書き方ではありますが、書いてしまえば必要なときだけ呼び出されるというものなので、安心して使うことができます。CodePenに書いているものと重複しますが、先ほどのサンプルの該当部分はこんな感じ。
```js
// 状態を表示する場所（破線の上のtrue or false）
const checkArea =  document.querySelector('#checkArea');

// 監視する領域（破線のところ）
const scrollArea = document.querySelector('#scrollArea');

// 監視する対象（赤い矩形）
const targetArea = document.querySelector('#targetArea');

// IntersectionObserverの設定
const options = {
  // 監視する領域をセット
  root: scrollArea
}

// コールバックの受け取り
const callback = (entries, observer) => {
  entries.forEach(entry => {
    checkArea.innerText = entry.isIntersecting;
  });
};

// IntersectionObserverオブジェクトを作る
const observerObject = new IntersectionObserver(callback, options);

// 監視対象の監視を開始する
observerObject.observe(targetArea);
```

# 無限スクロールがかんたんに作れるようになります

ではIntersection Observer APIを使うと具体的に何が作れるのか？ということですが、わかりやすのはスクロールで追加の要素を読み込む無限スクロールとかになると思います。
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="yLEwrap" data-user="kaitou1192" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/kaitou1192/pen/yLEwrap">
  Intersection Observer APIで無限スクロール</a> by Kaitou (<a href="https://codepen.io/kaitou1192">@kaitou1192</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>

最初に1〜20まで表示していますが、その後、フッタの端が表示されたら要素を5つずつ追加してフッタが画面外に出る。スクロールして、再びフッタの端が表示されたら追加で5つ読み込むというのを繰り返して、スクロールするだけで無限に要素が増えるというものです。

```js
// 監視する領域（ブラウザの表示領域）
const scrollArea = document;

// 監視する対象（フッタ）
const targetArea = document.querySelector('footer');

// IntersectionObserverの設定
const options = {
  // 監視する領域をセット
  root: scrollArea
}

// コールバックの受け取り
const callback = (entries, observer) => {
  entries.forEach(entry => {
    if(entry.isIntersecting) {
      // olの中にliを5つ追加する
      const orderedList = document.querySelector('ol');

      for (let index = 0; index < 5; index++) {
        const listElement = document.createElement('li');
        orderedList.appendChild(listElement);
      }

      // 監視対象の監視を再チェック
      observerObject.disconnect();
      observerObject.observe(targetArea);
    }
  });
};

// IntersectionObserverオブジェクトを作る
const observerObject = new IntersectionObserver(callback, options);

// 監視対象の監視を開始する
observerObject.observe(targetArea);
```

こちらでもスクロール量やウィンドウサイズについて取得していたり、トリガーが引かれすぎるので間引くみたいな処理を入れなくても実用的な動きをしていると思います。Intersection Observer API便利。

ちなみに下記の部分は要素を5つ追加してもFooterが画面の領域外に行ってくれなかった場合に対応するために書いています。
```js
observerObject.disconnect();
observerObject.observe(targetArea);
```
`observerObject.disconnect()` で、一旦監視を止め `observerObject.observe(targetArea)` で監視を再開することによって状態のチェックをしています。画面内にある場合は `entry.isIntersecting` が `true` になるため `li` を再び追加 `false` の場合はそこで止まります。

無限スクロール以外にも、ヘッダがスクロールで画面外に出たら、特定の要素を画面に固定とかもできそうな気はするのですが、そちらはCSSの `position: sticky;` でやるのが簡単なので、まずはそちらで検討してみるのが良いかもしれません。


# Masonryレイアウトで各カードを良い雰囲気で出してみたい

要素が入ってきたところで良い感じに表示したいケースとしては、こんな感じでMasonryレイアウトにエフェクトをかけるというのはぱっと思いつくかもしれません。こういうものもIntersection Observer APIで作れます。
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="poKBrpp" data-user="kaitou1192" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/kaitou1192/pen/poKBrpp">
  Untitled</a> by Kaitou (<a href="https://codepen.io/kaitou1192">@kaitou1192</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

```js
// 監視する領域（ブラウザの表示領域）
const scrollArea = document;

// 監視する対象（data-orderの値順にtargetObjectsに入れています）
const targetObjects = [];
const targetLists = document.querySelectorAll('li');
for (let index = 0; index < targetLists.length; index++) {
  targetObjects.push(document.querySelector('[data-order="' + (index + 1) + '"]'));
}

// IntersectionObserverの設定
const options = {
  root: scrollArea,
  threshold: 0.3
}

// コールバックの受け取り
const callback = (entries, observer) => {
  let transitionCounter = 0;
  entries.forEach(entry => {
    if(entry.isIntersecting) {
      transitionCounter++;
      observerObject.unobserve(entry.target);

      setTimeout(() => {
        entry.target.classList.add('active');
      }, transitionCounter * 300);
    }
  });
};

// IntersectionObserverオブジェクトを作る
const observerObject = new IntersectionObserver(callback, options);

// 監視対象の監視を開始する
targetObjects.forEach(object => {
  observerObject.observe(object);
});
```

<br>

肝になるところを抜粋して補足します。

`document.querySelectorAll('li')` を直接 `targetObjects` に入れてしまうと、HTML上で登場した順に `observe` が設定されてしまうため、今回のHTMLの書き方だと、左列の上から下、次に中央列の上から下、最後に右列の上から下になってしまいます。今回は、手動で表示して欲しい順（左上から並べる）に `data-order` に値を入れて、その順に `targetObjects` に格納し、最後に `targetObjects` に入っているものに対して　`observe`　を設定するということをしています。
```js
// 監視する対象（data-orderの値順にtargetObjectsに格納）
const targetObjects = [];
const targetLists = document.querySelectorAll('li');
for (let index = 0; index < targetLists.length; index++) {
  targetObjects.push(document.querySelector('[data-order="' + (index + 1) + '"]'));
}
```
```js
// 監視対象の監視を開始する
targetObjects.forEach(object => {
  observerObject.observe(object);
});
```

<br>

`threshold` は、0〜1までを指定でき、1は100%領域内に入ったときトリガーが引かれます。今回は0.3にしているので、30%が領域に入ったタイミングということになります。
```js
threshold: 0.3
```

# 罠 その1

Intersection Observer APIは、たしかに `addEventListener` の `scroll` に比べたらケアすることも少なく、軽いのですが、無神経に作り、無造作に扱うとこんなことが発生します。
![qiita_罠サンプル.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620149/b66611bc-7809-39ad-872a-0572080a417c.png)

上記のアニメーションのサンプルでは `threshold` をこまめに設定し、トリガーが呼ばれるたびに `console.log(entry)` で、監視している要素の情報を表示、それを激しくスクロールさせるということをしています。つまり必要のなくなった監視対象は `observerObject.unobserve` を使用して、監視対象から外してあげるということをCodePenのサンプルの方ではやっています。（負荷を高めるために入れた `console.log(entry)` も外しています。）
```js
observerObject.unobserve(entry.target);
```

ちなみに負荷が重くなる方のサンプルはこちら。
https://codepen.io/kaitou1192/pen/vYrMeze


使用する際には、いつ監視開始していて、いつ監視終了にするのかということを注意しておくと良いと思います。僕はとある実案件で、上記のことを雑にあつかっていた結果、お客さんの端末で謎のカクつきが発生するという事態が発生して肝を冷やしました。（監視し続けるケースの場合は、負荷が高くなりすぎないように気を使いましょう。）

# 罠 その2

今までのMasonryレイアウトのサンプルでは、Intersection Observer APIで監視するのは `li` で、実際にCSSでニュアンスを付けているのは `li::before` でした。これは '::before' が、重要というわけではなくて、監視する要素とCSS等で動きをつけるのを別の要素に分けているということがポイントで、監視する要素と動きを付ける要素を同じにしてしまうと、制御が辛くなることが多いです。

今回は `transform: translateY(30px);` → `transform: translateY(0);` でやっていますが `top: 30px;` → `top: 0;` でやったとしても、おそらく同じことになると思います。


# まとめ

Intersection Observer APIは、IEが対象外になった環境では、とても使える機能の1つですが、油断するとちょっとした罠もあるよというお話しでした。便利で応用が効くので、ぜひ一度試してみてください。