コラム

[Vue.js]無限スクロールプラグイン使用時の注意点(vue-infinite-loading)

vue-infinite-loadingプラグインを使用した時に注意すべき点をまとめてみようと思います。
このプラグインは便利ですが、正常に動作させるには正しく使用する必要があります。
私が認識している注意点4つを説明していこうと思います。

・初期化時にテーブルの要素へのアクセス
・ソート時の処理
・再検索した時の処理
・レコード追加の処理

サンプルコード

次のコードを元に説明していきます。

<template>
  <div>
		<div>
			<table>
				<thead>
					<tr>
						<th>ID</th>
						<th>名前</th>
						<th>カテゴリ</th>
						<th>値段</th>
					</tr>
				</thead>
				<tbody>
					<tr ref="row" v-for="(data) in viewData" :key="data.id">
						<td ref="id">{{ data.id }}</td>
						<td ref="name">{{ data.name }}</td>
						<td ref="category">{{ data.category }}</td>
						<td ref="price">{{ data.price }}</td>
					</tr>
				</tbody>
			</table>
			<!-- InfiniteLoadingコンポーネントを定義 -->
			<infinite-loading @infinite="infiniteHandler" spinner="spiral" >
				<div slot="no-more"></div>
			</infinite-loading>
		</div>
  </div>
</template>

<script>
export default {
	name: 'InfiniteScroll',
	data(){
		return {
			viewData: [],
			allData: [],
			offset: 0,
			limit: 20,
			itemCounter: 1
		}
	},
	
	mounted(){
		console.log("mounted")
	},

	created(){
		const list = [
			{name:'データモデリング入門', category:'IT本', price:'4000'}, 
			{name:'プロヘクトマネージャ', category:'IT本', price:'3000'},
			{name:'多変量分析', category:'統計本', price:'2000'},
			{name:'ソフトウェアテスト', category:'IT本', price:'3500'},
			{name:'エルデンリング', category:'PS5ゲーム', price:'8000'},
			{name:'GTA5', category:'PS4ゲーム', price:'7500'},
			{name:'マリオカート', category:'Switchゲーム', price:'5500'},
			{name:'スプラトゥーン', category:'Switchゲーム', price:'6000'}
		]
		for(let i = 0; i < 200; i++){
			var index = Math.floor(Math.random()*list.length)
			this.allData.push({count: this.itemCounter++, id: "S" + i, name: list[index].name, category: list[index].category, price:list[index].price})
		}
	},

	
	methods: {
		//InfiniteScrollから呼ばれるハンドラ、ここにデータ取得処理を実装
		infiniteHandler($state) {
			console.log("infiniteHandler")
		
			var data = this.getDatas()
			if (data == null || data.length == 0) {
				// 表示するデータが無くなった場合
				$state.complete()
			}else{
				// 表示するデータがある場合
				this.offset += data.length
				this.viewData = this.viewData.concat( data )
				$state.loaded()
			}
		},
		//axiosとかのAPI想定
		getDatas() {
			var data =  this.allData.slice(this.offset, this.offset + this.limit)
			return data
		}
	}
}
</script>


初期化時にテーブルの要素へのアクセス

テーブル初期表示時にテーブルの特定のセルの色を変えたいなどの処理をしたい時、通常のテーブルで
あれば「mounted」内でやると思いますが、このプラグインを使用する場合、「mounted」内でやると
要素がないと言われます。

次の処理を「mounted」内に追加

    this.$refs.id[0].style.background="#C0C0C0"

左上のセルの色をシルバー色に変えようとしていますが、要素が無いと言われます。
vue-infinite-loadingでハンドラー(infiniteHandler)を指定していますが、このメソッドでデータの取得、設定を
行っています。このメソッドは初期表示時に「mounted」より後に呼ばれるので「mounted」内でテーブルセル
にアクセスしても要素がない状態になるというのが理由です。

では「infiniteHandler」が最初に呼ばれた時に初期化すればよいのですが、次の用に記述しても要素がない可能性が
あります。

		infiniteHandler($state) {
			console.log("infiniteHandler")
		
			var data = this.getDatas()
			if (data == null || data.length == 0) {
				// 表示するデータが無くなった場合
				$state.complete()
			}else{
				// 表示するデータがある場合
				this.offset += data.length
				this.viewData = this.viewData.concat( data )
				$state.loaded()
     
        //追加部分 開始
        if( (this.offset - data.length) == 0 ){
          this.$refs.id[0].style.background="#C0C0C0" 
        }
        //追加部分 終了
			}
		}

上記の実装でも「$state.loaded()」が呼ばれて要素に反映されるまで非同期で行われているので、要素がない可能性
があるので次の用に実装しています。

		infiniteHandler($state) {
			console.log("infiniteHandler")
		
			var data = this.getDatas()
			if (data == null || data.length == 0) {
				// 表示するデータが無くなった場合
				$state.complete()
			}else{
				// 表示するデータがある場合
				this.offset += data.length
				this.viewData = this.viewData.concat( data )
				$state.loaded()
     
        //追加部分 開始
        this.$nextTick(function() {
          if( (this.offset - data.length) == 0 ){
            this.$refs.id[0].style.background="#C0C0C0" 
          }
        });
        //追加部分 終了
			}
		}

「this.$nextTick」はDOM更新の際に呼ばれるコールバックを記述します。
ここに初期化処理を記述する事で要素がない状態は回避する事が可能です。

ソート時の処理

ソート時はスクロール状態を維持するか、スクロールを初期状態にするかによって処理が変わってきます。
価格のソート処理を例に説明していきます。

上記のサンプルコードに次の処理を追加

methodsに「sortPrice」を追加
TODO部分の処理をスクロール維持、非維持により変えます。

		//価格のソート処理
		sortPrice(){
			//元のソート処理
			this.allData.sort(function(first, second){
				if (first.price > second.price){
					return -1;
				}else if (first.price < second.price){
					return 1;
				}else{
					return 0;
				}
			});
			
      //TODO:スクロール維持、非維持によりここの記述する処理を変える
		}

htmlのテーブルの価格部分のヘッダーを押下したら上記メソッドが呼ばれるようにします。

    <thead>
        <tr>
           <th>ID</th>
           <th>名前</th>
           <th>カテゴリ</th>
           <th @click="sortPrice">値段</th>
       </tr>
    </thead>

スクロール状態維持する処理

・以下の処理をサンプルコードからの変更点1のTODO以下に実装します。

    //TODO:スクロール維持、非維持によりここの記述する処理を変える
    let length = this.viewData.length
    for( let i = 0; i < length; i++ ){
        this.viewData.splice(i, 1, this.allData[i])
    }

現在表示している件数分、ソート後のデータと入れ替える処理です。
このように処理を実装するとスクロール状態は保たれます。

初期状態に戻す処理

・infinite-loadingタグとdataに変更を加える

	<infinite-loading @infinite="infiniteHandler" spinner="spiral" :identifier="infiniteId"> 
		<div slot="no-more"></div>
	</infinite-loading>
	data(){
		return {
			viewData: [],
			allData: [],
			offset: 0,
			limit: 20,
			itemCounter: 1,
			infiniteId:0
		}
	},

「:identifier="infiniteId"」の記述を追加し、dataに「infiniteId」を追加

・「sortPrice」メソッドのTODO以下に次のコードを追加

  //TODO:スクロール維持、非維持によりここの記述する処理を変える			
  this.viewData = []
  this.offset = 0
	if( this.infiniteId == 0 ){
		this.infiniteId = 1
	}else{
		this.infiniteId = 0
	}

・表示用のデータ配列を空にします。
・this.infiniteIdの値を変更をします。この処理によりスクロールがリセットされます。

再検索した時の処理

再検索時もスクロール状態をリセットする事が望ましいと思われますので、「ソート時の処理の初期状態に戻す処理」の時と同じような記述が良いと思います。

レコード追加の処理

テーブル行の要素にアクセスする時に$refsを使用している場合に問題になる処理があります。
追加するレコードが最終行であれば問題ないのですが、それ以外の箇所へ追加が行われると要素自体は追加した箇所
にあるが、$refsでは配列の最後に追加されるという事象があります。

上記の用にDOM配列とRefsの配列がずれてしまいます。
このずれを解消する方法は、「v-for」文でkeyとしているIDの振り直しです。
サンプルコードを以下に書きます。

・データ取得メソッド内の変更

		getDatas() {
			var data =  this.allData.slice(this.offset, this.offset + this.limit)
			data.forEach((value) => {
				value.count = this.itemCounter++
			})
			return data
		},

取得したタイミングで「v-for」のkeyで使用する「count」プロパティを設定するように変更

・行追加メソッド追加

		addRow(){
			var data = {count:0, id: '', name: '', category: '', price:''}

			this.viewData.unshift(data)
			this.allData.unshift(data)

			this.itemCounter = 1
			this.viewData.forEach((value) => {
				value.count = this.itemCounter++
			})
			
		  this.offset += 1

			this.$nextTick(function() {
				//refs確認用コード
				this.$refs.row[0].style.background="#C0C0C0"
				this.$refs.row[1].style.background="#b0e0e6"
			})

		}

・行追加メソッドを追加しボタン押下と関連付けます。
・空のデータを全てのデータ配列と表示用データ配列の先頭に追加します。
(サーバから都度取得する場合は全てのデータ配列には追加するのでは無く、サーバ側に追加処理等が必要)
・表示用の「count」プロパティの振り直しをします。
・「this.$nextTick」内はrefsが正常に並んでいるかの確認用コードです。

以上、「vue-infinite-loading」を使用する上での注意点を書きました。
他にもデータが大量にある場合、DOM要素が増え続けるなどの問題がありますが、それは他の無限スクローラーと
比較して記事にしたいと思います。
参考になれば幸いです。

この記事をシェアする
  • Facebookアイコン
  • Twitterアイコン
  • LINEアイコン

お問い合わせ ITに関するお悩み不安が少しでもありましたら、
ぜひお気軽にお問い合わせください

お客様のお悩みや不安、課題などを丁寧に、そして誠実にお伺いいたします。

お問い合わせ
お電話でのお問い合わせ 03-5820-1777(平日10:00〜18:00)
よくあるご質問