はじめに
vueで無限スクロールを実装するとそのページに戻ってきた時に以前スクロールした位置までスクロールした状態で表示したい状況に遭遇します。
要件
無限スクロール状況の再現に際して考慮したい点は、
- 無限スクロールのページに戻ってきた時に
- 取得処理の完了を待ってスクロール処理を行うこと
- backで戻ってきたのか、reloadで戻ってきたのか、pushで戻ってきたのかの判別
の2点になります。
実装
今回vue-history-state
というライブラリを使いました。
いつも有難く使わせて頂いておりますm(_ _)m
使い方の詳細についてはREADMEや作者の方の解説記事をご参照下さい。
まず、初期化します。
import { createApp } from 'vue';
import HistoryStatePlugin from 'vue-history-state';
const app = createApp();
app.use(HistoryStatePlugin, { maxHistoryLength: 5 });
関連する処理をcomposablesにまとめて切り出します。
vue-history-state
の嬉しいところは、状態の保存処理が離脱時に行われるため、必要な処理が複数ページにまたがることなく無限スクロールを実装するページにのみ書けば済む点です。
そのため、一つのcomposableにまとめることも容易になりました。
処理の大枠としては、
- 離脱時に状態を保存
- 戻ってきた時に
checkHistoryState
を実行し、状態が保存されていればisComeback
フラグをtrue
にする - 保存されていた
currentPage
とperPage
を元に必要な数だけコンテンツをAPIから取得する - 取得完了後に
restorePagerAndScroll
を実行し、currentPage
とperPage
を本来の数に戻す
なお、今回はpush
で戻ってきた時は再現させず、back
とreload
で戻ってきた時のみ再現するように実装しました。
import { ref, onBeforeUnmount, Ref } from 'vue';
import { useHistoryState, onBackupState } from 'vue-history-state';
export function usePager(initialPerPage: number) {
const isComeback = ref<boolean>(false);
const interval = ref<number | null>(null);
const currentPage = ref<number>(0);
const perPageCount = ref<number>(0);
const history = useHistoryState();
const checkHistoryState = () => {
if (history.action !== 'back' && history.action !== 'reload') return;
if (!history.data) return;
if (history.data.scrollY) isComeback.value = true;
if (!Number(history.data.perPage) || !Number(history.data.currentPage)) return;
currentPage.value = 1;
perPageCount.value = history.data.perPage * history.data.currentPage;
};
const restorePagerAndScroll = () => {
if (!isComeback.value) return;
isComeback.value = false;
currentPage.value = Math.ceil(perPageCount.value / initialPerPage);
perPageCount.value = initialPerPage;
interval.value = window.setInterval(() => {
if (
history.data?.scrollY &&
window.document.documentElement.scrollHeight >= history.data?.scrollY
) {
window.scrollTo(0, history.data?.scrollY);
if (interval.value) window.clearInterval(interval.value);
history.data = {};
}
}, 100);
};
onBackupState(() => ({
...(history.data || {}),
scrollY: window.scrollY,
currentPage: currentPage.value,
perPage: perPageCount.value,
}));
onBeforeUnmount(() => {
if (interval.value) window.clearInterval(interval.value);
});
return {
currentPage,
perPageCount,
checkHistoryState,
restorePagerAndScroll,
};
}
下記のように呼び出します。
<script setup lang="ts">
import { usePager } from '@/composables/pager';
const items = ref<any[]>([])
// このページでは一度の取得で16件ずつ取得するとする
const initialPerPage = 16;
const { checkHistoryState, restorePagerAndScroll } = usePager(initialPerPage);
const apiFetch = async () => {
await fetch('~').then((res) => {
items.value.push(...res)
restorePagerAndScroll();
});
};
onMounted(() => {
checkHistoryState();
});
</script>
<template>
<some-list :items="items" @refetch="apiFetch"/>
</template>
終わりに
今回は無限スクロールにおいてスクロール状況の再現にvue-history-state
を使いましたが、保存する値はなんでも良いため他にも色んな状況においてviewの状況を復元するための用途に使えます。
next用のnext-navigation-history
もあるそうなので、よかったら皆さんも使ってみてください