🐭

【Nuxt/Vue】マウント時に指定したコンポーネントまで正常にスクロールしない問題について|Offers Tech Blog

2022/09/05に公開

概要

こんにちは、Offers を運営している株式会社 overflow の Software Engineer(主戦場はフロントエンド)の Kazuya です。今回は、Nuxt2(Vue2)においてマウント時に指定したコンポーネントまで正常にスクロールしない問題について紹介します。

はじめに

本記事の内容は、弊社の環境で発生したもので、他の環境で再現されない可能性もあります。また、今回紹介するものは一例であり最適な実装でない可能性があります。これらを予めご理解の上、参考にしていただけると幸いです。

環境

  • Node: Node16
  • Framework: Nuxt(2.15.8)
  • CSS: CSS Modules(SCSS)

実現したいこと

マウント時に指定したコンポーネントまでスクロール

今回は、 マウント時に指定したコンポーネントまでスクロールさせたかったため、Vue のライフサイクルである mounted と HTML 要素を取得できる ref属性 を用いて実装することにしました。実際のコードは以下の通りです。

<template>
  <main>
    <section :class="$style.container_top"></section>
    <section ref="bottom" :class="$style.container_bottom">
      <ul>
        <li v-for="(item, index) in items" :key="index">
          {{ item }}
        </li>
      </ul>
    </section>
  </main>
</template>

<script lang="ts">
import Vue from "vue";

export default Vue.extend({
  components: {},
  props: {},
  data() {
    return {
      hash: this.$route.hash,
      items: [],
    };
  },
  head() {
    return {};
  },
  computed: {},
  mounted() {
    if (this.hash) {
      const refName = this.hash.replace("#", "");
      this.scrollToElement(refName);
    }
  },
  methods: {
    scrollToElement(refName: string) {
      const el = this.$refs[refName] as HTMLElement;
      el?.scrollIntoView({ behavior: "smooth" });
    },
  },
});
</script>

<style lang="scss" module>
.container_top {
  width: 100%;
  height: 100vh;
  background-color: red;
}
.container_bottom {
  width: 100%;
  height: 800px;
  background-color: blue;
}
</style>

ref は、定義した DOM にアクセスするため、マウント後でないと undefined になってしまいます。そこで「DOM の処理完了時」に発火するライフサイクルである mounted 内でアクセスするようにしています。今回の場合は、hash に定義された値を ref の値として用いて、スクロールさせる形になっています。

上記のコードを実際に実行してみると、意図していた動作にはなりませんでした。ref が正常に取得できていないのが原因と考えて、scrollToElement 内で取得している ref をコンソールログで確認してみたところ DOM は取得できていることを確認できました。

試したこと

DOM 更新後に発火させることができれば、意図する挙動になるのではないかと考え、Vue.js の nextTick メソッドを使って実装してみることにしました。

mounted() {
 if (this.hash) {
   const refName = this.hash.replace('#', '');
   this.$nextTick(function () {
     this.scrollToElement(refName);
   });
 }
}

上記に書き換えて実行してみると...またまた動作しませんでした。

https://v3.ja.vuejs.org/api/instance-methods.html#nexttick

解決方法

色々調査しましたが、根本の解決方法をなかなか発見できずにいました。そこで苦肉の策として setTimeout で制御してみたところ、意図した挙動になりました。(謎)

scrollToElement(refName: string) {
  const el = this.$refs[refName] as HTMLElement;
  setTimeout(() => {
     el?.scrollIntoView({ behavior: 'smooth' });
  });
}

安定した動作が保証できない setTimeout を用いた実装はあまりしたくないですが、他に良い改善策が見つからなかったため、妥協することにしました。この方法以外で改善できる方法をご存じの方がいれば、コメントなどでご教授いただけると幸いです。

まとめ

今回は自身が実装時に詰まってしまった、「マウント時に指定したコンポーネントまでスクロール」の実装方法について紹介しました。React だとかなり簡単に実装できるのですが、Vue はライフサイクル周りが複雑でこういう系の実装に思ったより時間がかかってしまう傾向があるように感じています...。同じように詰まっている方の助けになれば幸いです。

本記事を最後まで読んで頂き、ありがとうございました。「こんな記事を書いてほしい!」などありましたらコメントいただけると幸いです。

おまけ

非同期でデータを取得してからスクロールさせる場合

上記の方法は DOM の高さが変化ない場合に使えますが、API など非同期でデータを取得してそのデータを表示させる場合は、DOM の高さも動的に変化します。そのため、初期段階では高さが 0 で正常にスクロールしないケースもあります。また、Vue.js の v-if で表示制御を行っている場合にも、動作しない場合があります。(v-if は DOM 自体を消しているので、ref が取得できない)

このように様々な条件で動作しなくなるリスクがあるため、実装時に考えることも増え結果的に複雑になりやすいと思います。そこで再び、あまりやりたくない実装シリーズが登場します。

const interval = setInterval(() => {
  const el = this.$refs[refName] as HTMLElement;
  if (el?.offsetTop) {
    el?.scrollIntoView({ behavior: "smooth" });
    clearInterval(interval);
  }
}, 500);

本編で紹介したものは、setTimeout を使用していましたが、今回のものは setInterval に変更しています。これにより、v-if や非同期で動的に DOM が変化するケースにも比較的対応しやすくなります。ただ、これも安定した動作が保証できないため、苦肉の策であることに代わりはありません。

正攻法で行くのであれば、v-ifv-show に書き換え、非同期の処理完了時にメソッドを実行する感じになると思います。ただこの場合、実装コストが高くなるため、品質とスピードどちらを重視するのか判断して、適切な方を選択してもらえればいいかなと思います。

関連記事

https://zenn.dev/offers/articles/20220523-component-design-best-practice
https://zenn.dev/offers/articles/20220418-what-is-bff-architecture
https://zenn.dev/offers/articles/20220623-nuxt3-server-ua

Offers Tech Blog

Discussion