20
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

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

要件

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

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

の2点になります。

実装

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

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

まず、初期化します。

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

20
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?