Vue 3 SFCとIntersection Observer APIでエンドレスのデータストリームを

## 目的
Vue 3のSFCでIntersection Observer APIを使って、ページの最後にスクロールした際に、情報取得を自動で行う仕組みを作る

## 目次
1. 背景
1. Observer部品を作る
1. List部品を作る
1. エンドレスにする
1. まとめ

## 背景
Intersection Observer APIを従来のVueで使う方法を紹介する記事が多いが、新しいVue 3 SFCでの使い方が少し違うので、自分の解決方法を紹介しようと思います。
<a href="https://vueschool.io/articles/vuejs-tutorials/build-an-infinite-scroll-component-using-intersection-observer-api/">Alex Moralesのこの記事</a>、そして<a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">MDNのドキュメント</a>も参考にしています。

## Observer部品を作る
エンドレスのリストを作るために、ページの最後までスクロールした時にイベントが起きて、データが取得されなければならない。
ページの最後までスクロールした時に、Eventが発生すれば、データをサーバーから取得して、表示のリストにPushすればいいです。
なので、そのEventを発生させる部品を作ります。

```tsx:Oberserver.vue
<script setup lang="ts">
import { onMounted, ref } from "vue";

const emit = defineEmits<{ (e: "page-end"): void }>();

const end = ref<HTMLDivElement>();

onMounted(() => {
  const observer = new IntersectionObserver(
    (entries) => {
      const firstEntry = entries[0];
      if (firstEntry.isIntersecting) {
        console.log(firstEntry);
        emit("page-end");
      }
    },
    {
      root: document,
      threshold: 0,
      rootMargin: "0px",
    }
  );
  observer.observe(end.value);
});
</script>

<template>
  <div ref="end" />
</template>

```
Intersection Observerを作る時に、引数として、callback関数と、設定値のオブジェクトを渡します。

callback関数ですが、Oberserverが観察しているElementが見えてきた時に実行されます。
そして、登録されていて尚且つ見えている全てのElementが引数にArrayとして渡されるので、Arrayを解梱さんだれーならん。

Arrayの中身には、登録されたElementについて様々な情報が入っていますが、今回気になっているのは、isIntersecting（画面に差し掛かって、入ってきているかどうか）のbooleanです。

```typescript
(entries) => {
      const firstEntry = entries[0];
      if (firstEntry.isIntersecting) {
        console.log(firstEntry);
        emit("page-end");
      }
    }
```
差し掛かっていれば、Vue内で"page-end"というEventを発生させましょう！


設定値のオブジェクトですが、
1. root: Observerが見る枠（今回はページ全体でObserverに見てほしいのでdocumentを渡す）
1. threshold: 対象物のElementがどれくらい見えていればcallback関数を実行するかを設定する（0で1ピクセルでも見えれば実行される）
1. rootMargin: Observerが見る枠を調整する（"xxpx"で指定する）

```typescript
{
   root: document,
   threshold: 0,
   rootMargin: "0px",
}

```




#### また、もう一つ工夫があります。
observerのオブジェクトを作った後、観察する対象物を登録しなければならない。
Vue 3のSFCでは、refを使ってDOMにアクセスすることがベストなので、endというrefを作って、それを登録します。

## List部品を作る

List部品には https://jsonplaceholder.typicode.com/comments のAPIを使います。

stateではページ目と、表示されるコメント情報を保持する。

fetchDataの最後に、取得したデータをstateに保存する時ですが、state内の既存のArrayを解梱した上で、取得したArrayも解梱して新しいArrayにしてから上書きします。

これで50個分のコメントを読み込んで表示されます。

```tsx:EndlessList.vue
<script setup lang="ts">
import { onMounted, reactive } from "vue";

const state = reactive<{
  page: number;
  data: {
    postId: number;
    id: number;
    name: string;
    email: string;
    body: string;
  }[];
}>({ page: 1, data: [] });

const fetchData = async () => {
  const results = await fetch(
    `https://jsonplaceholder.typicode.com/comments?_page=${state.page}&_limit=50`
  );

  const json = await results.json();

  state.data = [...state.data, ...json];
};

onMounted(async () => fetchData());

</script>

<template>
  <div class="card" v-for="comment in state.data" :key="comment.id">
    <h1>{{ comment.name }}</h1>
    <p>{{ comment.body }}</p>
  </div>
</template>

<style>
.card {
  width: 90%;
  padding: 1rem;
  margin: 1rem auto 0 auto;
  background-color: white;
  border: 1px solid white;
  border-radius: 8px;
  box-shadow: 1px 2px 8px rgba(0, 0, 0, 0.226);
}
</style>

```

## エンドレスにする
上記に作ったObserver.vueの出番が来ました。

先ほど作ったリストの最後にObserver部品を入れれば、ページの最後までスクロールした時にEventが発生します。

余談ですが、v-if="state.data.length"を追加しているのは、最初のデータが読み込まれるまで、Observer部品が見えてしまうので、リストが表示されるまで出さないでおきます。

```tsx:EndlessList.vue
<script setup lang="ts">
import { onMounted, reactive } from "vue";
import Observer from "./Observer.vue";
import BaseCard from "./BaseCard.vue";

const state = reactive<{
  page: number;
  data: {
    postId: number;
    id: number;
    name: string;
    email: string;
    body: string;
  }[];
}>({ page: 1, data: [] });

const fetchData = async () => {
  const results = await fetch(
    `https://jsonplaceholder.typicode.com/comments?_page=${state.page}&_limit=50`
  );

  const json = await results.json();

  state.data = [...state.data, ...json];
};

onMounted(async () => fetchData());
</script>

<template>
  <div class="card" v-for="comment in state.data" :key="comment.id">
    <h1>{{ comment.name }}</h1>
    <p>{{ comment.body }}</p>
  </div>
  <Observer v-if="state.data.length" />
</template>
```

このままページの最後までスクロールしてみると、
![スクリーンショット 2022-02-08 16.10.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/64162fe0-8817-dd0d-c461-a7ada3c81ff5.png)
Eventが発生して、consoleに無事にログされます！

ここで、fetchDataをもう一度実行すればいいわけです。

```tsx:EndlessList.vue
<script setup lang="ts">
import { onMounted, reactive } from "vue";
import Observer from "./Observer.vue";

const state = reactive<{
  page: number;
  data: {
    postId: number;
    id: number;
    name: string;
    email: string;
    body: string;
  }[];
}>({ page: 1, data: [] });

const fetchData = async () => {
  const results = await fetch(
    `https://jsonplaceholder.typicode.com/comments?_page=${state.page}&_limit=50`
  );

  const json = await results.json();

  state.data = [...state.data, ...json];
};

onMounted(async () => fetchData());

const loadMorePosts = () => {
  state.page++;
  fetchData();
};
</script>

<template>
  <div class="card" v-for="comment in state.data" :key="comment.id">
    <h1>{{ comment.name }}</h1>
    <p>{{ comment.body }}</p>
  </div>
  <Observer v-if="state.data.length" @page-end="loadMorePosts" />
</template>
```
state.page++でページのメモリーも増やさんだれーならんどー！

## まとめ

これでIntersection Observerを使って簡単にエンドレスのリストを実装しました。本当は、Intersection Observerにはこれ以外に色々な使い方があって、もっと追求すれば、できることが増えると思います！