I am building an infinite scroll feature in a Vue3 app. Everything works fine except I can't find a way to push more data when the user has scrolled to the end of the page.
All Profiles are loaded into Vuex initially, but only rendered batch by batch as the user keeps scrolling down.
I want to first render an initial number of Profiles, than push a number of new Profiles to the data that holds the array, once the user has reached the page bottom.
I need to use a computed property initially as this will wait until the Vuex array of Profiles has loaded from the database. If I use a hook, the risk is that the data has not been loaded yet. Also the computed property will recalculate everytime new data is added.
So the v-for directive is bound to this computed property.
But how do I push new Profiles to this computed property? I tried to assign this computed property to a component data property, but this is not how it works apparently.
Any help much appreciated.
<template>
<div v-for="profile in loadedProfiles" :key="profile.id">
{{ profile.name }}
</div>
</template>
<script>
export default {
data: () => ({
loadedProfiles: this.computedLoadedProfiles()
}),
computed: {
computedLoadedProfiles() {
if (this.$store.state.numberOfProfilesLoaded == this.$store.state.numberOfProfilesLoadedInitially) {
return this.$store.state.currentProfileList.slice(0, this.$store.state.numberOfProfilesLoadedInitially);
}
},
methods: {
loadMoreProfiles() {
if($store.state.scrolledToBottom) {
loadedProfiles.push(...) //push more profiles to loadedProfiles
}
}
}
},
}
</script>
<style>
</style>
CodePudding user response:
In Vue, you can use the $set method to add an item to an array in a computed property. Here is an example of how you could implement this in your code:
<template>
<div v-for="profile in loadedProfiles" :key="profile.id">
{{ profile.name }}
</div>
</template>
<script>
export default {
computed: {
computedLoadedProfiles() {
if (this.$store.state.numberOfProfilesLoaded == this.$store.state.numberOfProfilesLoadedInitially) {
return this.$store.state.currentProfileList.slice(0, this.$store.state.numberOfProfilesLoadedInitially);
}
}
},
methods: {
loadMoreProfiles() {
if (this.$store.state.scrolledToBottom) {
// Use the $set method to add an item to the array
this.$set(this.computedLoadedProfiles, this.computedLoadedProfiles.length, ...);
}
}
}
}
</script>
Note that you will need to make sure that the computedLoadedProfiles array is reactive so that Vue knows to update the view when you add an item to the array. In the example above, the computedLoadedProfiles array is made reactive because it is returned by the computedLoadedProfiles computed property, which is automatically reactive in Vue.
CodePudding user response:
Here's the store mechanics:
const { createApp, onMounted, computed } = Vue;
const { createStore } = Vuex;
const store = createStore({
state: {
profiles: [],
visibleProfilesCount: 0,
loading: false
},
actions: {
// replace with actual call to server
loadMoreProfiles({ commit, state }) {
commit('setLoading', true);
return new Promise((resolve) => {
setTimeout(() => {
commit(
"addProfiles",
Array.from({ length: 30 }).map(
(_, key) => key state.profiles.length
)
);
commit('setLoading', false);
resolve();
}, 1e3);
});
},
},
mutations: {
addProfiles(state, profiles) {
state.profiles.push(...profiles);
},
setLoading(state, loading) {
state.loading = loading;
},
showMoreProfiles(state) {
state.visibleProfilesCount = 10;
},
},
getters: {
visibleProfiles(state) {
return state.profiles.slice(0, state.visibleProfilesCount);
},
},
});
const app = createApp({
setup() {
const showMore = () => {
store.commit("showMoreProfiles");
if (store.state.profiles.length < store.state.visibleProfilesCount) {
store.dispatch("loadMoreProfiles");
}
};
onMounted(showMore);
return {
visibleProfiles: computed(() => store.getters.visibleProfiles),
loading: computed(() => store.state.loading),
showMore,
};
},
});
app.use(store);
app.mount("#app");
.logger {
position: fixed;
width: 50vw;
top: 0;
bottom: 0;
right: 0;
}
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/vuex@4"></script>
<div id="app">
<div v-for="profile in visibleProfiles" :key="profile">{{ profile }}</div>
<div v-if="loading">Loading...</div>
<button v-else @click="showMore">Load more</button>
<pre v-text="JSON.stringify({
profiles: $store.state.profiles.length,
visibleProfiles: $store.state.visibleProfilesCount,
loading
}, null, 2)"></pre>
</div>
All that's left is to link showMore
to the scroll action, instead of the button, and replace loadMoreProfiles
action with an actual call to server to get the more profiles and add them to state.
Obviously, you don't have to keep visibleProfilesCount
and visibleProfiles
in the store. They can be declared in your component, along these lines:
const visibleProfilesCount = ref(0)
const visibleProfiles = computed(
() => store.state.profiles.slice(0, visibleProfilesCount.value)
)
const showMore = () => {
visibleProfilesCount.value = 10;
if (store.state.profiles.length < visibleProfilesCount.value) {
store.dispatch("loadMoreProfiles");
}
};
The gist of it is: visibleProfiles
is computed
, or getter
on store, (which means derived state), resulting from two other state properties: profiles
and visibleProfilesCount
. Whenever one of the state props changes, the computed
changes.