intersection observerを使用したInfinite Scrollの実装

この記事は [CyberAgent 20 新卒 Advent Calendar 2019](https://adventar.org/calendars/3976) の 22 日目です！
今回はタイトルにも書いてありますがintersection observerを使用してInfinite Scrollを実装したいと思います。

# Intersection Observerとは

> Intersection Observer API (交差監視 API) は、ターゲットとなる要素が、祖先要素もしくは文書の最上位のビューポートと交差する変更を非同期的に監視する方法を提供します。
https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API



# useObserver
- intersection observerのcustom hook

```typescript
import React, { useEffect, useState, RefObject } from 'react';
function useObserver(
  ref: RefObject<HTMLDivElement>,
  ops?: IntersectionObserverInit
) {
  const [intersect, setIntersect] = useState<boolean>(false);
  useEffect(() => {
    function callback(entries: IntersectionObserverEntry[]) {
      entries.forEach(entry => {
        setIntersect(entry.isIntersecting);
      });
    }
    const observer = new IntersectionObserver(callback, ops);
    if (ref.current === null) {
      return;
    }
    observer.observe(ref.current);
    return () => {
      if (ref.current !== null) {
        observer.unobserve(ref.current);
      }
    };
  });
  return intersect;
}
```


# 使い方
```typescript
export const Index: React.FC = () => {
  const target = React.useRef<HTMLElement>(null);
  const intersect = useObserver(target);

  return (
    <React.Fragment>
      <div style={{ position: 'fixed', top: 0, left: '50%' }}>
        {intersect ? 'visible' : 'not visible'}
      </div>
      <div style={{ height: 1000 }}></div>
      <div ref={target}></div>
    </React.Fragment>
  );
};
```

- まず、ObserveしたいElementを<code>useObserver</code>に渡すために、<code>ref</code>を作成します。<br>
  つぎに、ObserveしたいElementに<code>props</code>として<code>ref</code>を渡せば完了です。<br>
  今回の例では、最初は<code>ref</code>を渡したDiv Elementが画面に表示されていないため、画面上部中央には、<bold>not visible</bold>と表示されます。<br>
  画面をスクロールして、<code>ref</code>を渡したDiv Elementが画面に表示されると、<bold>visible</bold>と表示されます。

# infinite scroll
## 今回実装したもの
![20191013001653.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174688/82b57e10-1c45-a530-6f86-af1bdf84723a.gif)


## ソースコード
```typescript
import React, { useEffect, useState, RefObject } from 'react';
import axios from 'axios';
import './App.css';

interface Item {
  id: number;
  image: string;
}

const fetchItems = async (): Promise<Item[]> => {
  const { data } = await axios.get('http://localhost:3000/item');
  return data;
};

function useObserver(
  ref: RefObject<HTMLElement>,
  ops?: IntersectionObserverInit
) {
  const [intersect, setIntersect] = useState<boolean>(false);
  useEffect(() => {
    function callback(entries: IntersectionObserverEntry[]) {
      entries.forEach(entry => {
        setIntersect(entry.isIntersecting);
      });
    }
    const observer = new IntersectionObserver(callback, ops);
    if (ref.current === null) {
      return;
    }
    observer.observe(ref.current);
    return () => {
      if (ref.current !== null) {
        observer.unobserve(ref.current);
      }
    };
  });

  return intersect;
}

export const App: React.FC = () => {
  const [items, setItems] = useState<Item[]>([]);
  const target = React.useRef<HTMLDivElement>(null);
  const intersect = useObserver(target);

  useEffect(() => {
    async function fetch() {
      const data = await fetchItems();
      setItems(data);
    }
    fetch();
  }, []);

  useEffect(() => {
    async function fetch() {
      if (intersect) {
        const data = await fetchItems();
        setItems(prevItems => [...prevItems, ...data]);
      }
    }
    fetch();
  }, [intersect]);

  return (
    <React.Fragment>
      <div className="container">
        {items &&
          items.map((item) => {
            return (
              <React.Fragment key={item.id}>
                <div className="card">
                  <div className="card__item">
                    <img src={item.image}></img>
                  </div>
                </div>
              </React.Fragment>
            );
          })}
      </div>
      <div ref={target}>loading</div>
    </React.Fragment>
  );
};

export default App
```

# おわりに
今回はintersection observerを使用してInfinite Scrollの実装を行いました．
intersection observerはその他にも，[オフスクリーン時の画像の遅延読み込み](https://developers.google.com/web/tools/lighthouse/audits/offscreen-images)やコンポーネントの遅延読み込みなどの使用法が考えられます．
要素をobserveして何かするという処理を書くことは結構あると思うので，積極的に使用していきましょう