🌊

Vue3でスクロール状況を再現する

2023/12/22に公開

はじめに

vueで無限スクロールを実装するとそのページに戻ってきた時に以前スクロールした位置までスクロールした状態で表示したい状況に遭遇します。

要件

無限スクロール状況の再現に際して考慮したい点は、

  • 無限スクロールのページに戻ってきた時に
    • 取得処理の完了を待ってスクロール処理を行うこと
    • backで戻ってきたのか、reloadで戻ってきたのか、pushで戻ってきたのかの判別

の2点になります。

実装

今回vue-history-stateというライブラリを使いました。
いつも有難く使わせて頂いておりますm(_ _)m

https://github.com/hidekatsu-izuno/vue-history-state

使い方の詳細についてはREADMEや作者の方の解説記事をご参照下さい。

https://qiita.com/hidekatsu-izuno/items/8abad0c1bb559490d65b

まず、初期化します。

main.ts
import { createApp } from 'vue';
import HistoryStatePlugin from 'vue-history-state';
const app = createApp();
app.use(HistoryStatePlugin, { maxHistoryLength: 5 });

関連する処理をcomposablesにまとめて切り出します。
vue-history-stateの嬉しいところは、状態の保存処理が離脱時に行われるため、必要な処理が複数ページにまたがることなく無限スクロールを実装するページにのみ書けば済む点です。
そのため、一つのcomposableにまとめることも容易になりました。

処理の大枠としては、

  1. 離脱時に状態を保存
  2. 戻ってきた時にcheckHistoryStateを実行し、状態が保存されていればisComebackフラグをtrueにする
  3. 保存されていたcurrentPageperPageを元に必要な数だけコンテンツをAPIから取得する
  4. 取得完了後にrestorePagerAndScrollを実行し、currentPageperPageを本来の数に戻す

なお、今回はpushで戻ってきた時は再現させず、backreloadで戻ってきた時のみ再現するように実装しました。

composables/pager.ts
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,
  };
}

下記のように呼び出します。

some-list.vue
<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もあるそうなので、よかったら皆さんも使ってみてください

https://github.com/hidekatsu-izuno/next-navigation-history

GitHubで編集を提案

Discussion