目的
Vue 3のSFCでIntersection Observer APIを使って、ページの最後にスクロールした際に、情報取得を自動で行う仕組みを作る
目次
- 背景
- Observer部品を作る
- List部品を作る
- エンドレスにする
- まとめ
背景
Intersection Observer APIを従来のVueで使う方法を紹介する記事が多いが、新しいVue 3 SFCでの使い方が少し違うので、自分の解決方法を紹介しようと思います。
Alex Moralesのこの記事、そしてMDNのドキュメントも参考にしています。
Observer部品を作る
エンドレスのリストを作るために、ページの最後までスクロールした時にイベントが起きて、データが取得されなければならない。
ページの最後までスクロールした時に、Eventが発生すれば、データをサーバーから取得して、表示のリストにPushすればいいです。
なので、そのEventを発生させる部品を作ります。
<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です。
(entries) => {
const firstEntry = entries[0];
if (firstEntry.isIntersecting) {
console.log(firstEntry);
emit("page-end");
}
}
差し掛かっていれば、Vue内で"page-end"というEventを発生させましょう!
設定値のオブジェクトですが、
- root: Observerが見る枠(今回はページ全体でObserverに見てほしいのでdocumentを渡す)
- threshold: 対象物のElementがどれくらい見えていればcallback関数を実行するかを設定する(0で1ピクセルでも見えれば実行される)
- rootMargin: Observerが見る枠を調整する("xxpx"で指定する)
{
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個分のコメントを読み込んで表示されます。
<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部品が見えてしまうので、リストが表示されるまで出さないでおきます。
<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>
このままページの最後までスクロールしてみると、
Eventが発生して、consoleに無事にログされます!
ここで、fetchDataをもう一度実行すればいいわけです。
<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にはこれ以外に色々な使い方があって、もっと追求すれば、できることが増えると思います!