I am new to Vue and I am struggling with an extremely weird issue: In my component, in the creates() function I fetch data and save them into my items Array. I display the content of the items with v-for as a paragraph.
Now here comes the weird thing: When I start the App, nothing shows, although I can see in the Vue dev-tools that the proper data is stored in the items array. When I refresh, same happens. Only when I make changes in my code and save it, I see the proper data being displayed in the UI.
Code: WeeklyRanking.vue:
<script>
import { computed } from "vue"
import RankingItem from "./RankingItem.vue";
import {getUsersWithPoints} from "../../../data/data";
export default {
name: "WeeklyRanking",
data() {
return {
items: [],
};
},
created (){
getUsersWithPoints().then((users) => {
this.items = users;
console.log(users);
});
},
computed: {
sortedRankings() {
return [...this.items].sort((a, b) => b.points - a.points);
},
maxPoints() {
return 95;
}
},
components: { RankingItem },
}
</script>
<template>
<div >
<h1>Wochen - Ranking</h1>
<div >
<p v-for="r in items"> {{r.username}} </p>
{{items}}
</div>
<button>
<router-link to="/taskDone"> Aufgabe erledigt</router-link>
</button>
</div>
</template>
<style>
.weekly-ranking-container{
padding: 24px;
max-width: 400px;
min-width: 300px;
}
.ranking-chart{
display: flex;
flex-direction: column;
align-items: flex-start;
}
</style>
The console.log shows the following:
The Vue inspector show the data as follows:
What I have tried so far
Use fake data directly in the data() part - works. The UI shows the initial data that I mock here.
get fake data from data source with fake delay - works as well. Shows the proper data after the artificial delay. The interesting thing in this case, is that the Array in the console log looks different:
I could not make sense of the difference between those two logs, especially because the Vue inspector displays it exactly the same. Also, the part with the code saving does not make sense to me.
Below I show the data call code. I use firebase with the modular api. Thanks for your help!
Data call code:
async function getUsersWithPoints(){
// With the commented part it works
// const fakeUsers = [
// {username:"Marcelo", points:95, id:"PRirMre5IUeHP7BA08wh"},
// {username:"Sebse", points:80, id:"PRirMasdoiHP7BA08wh"},
// {username:"Simi", points:20, id:"yhaHRRxN7PFmfyHZqZS1"},
// ];
// await new Promise(r => setTimeout(r, 2000));
// console.log("FAKE USERS:");
// console.log(fakeUsers);
// return fakeUsers;
//with the below part it does not
let users = [];
const usersQuery = query(collection(db, `groups/${wgKey}/users`));
const usersSnap = await getDocs(usersQuery);
usersSnap.forEach(async(user)=>{
const tasksQuery = query(collection(db, `${user.ref.path}/tasks`));
const tasks = await getDocs(tasksQuery);
let points = 0;
tasks.forEach((task)=>{
points =task.data().points;
});
users.push({
username: user.data().name,
points: points,
id: user.id
});
});
return users;
}
```
CodePudding user response:
I can remember having an identical issue with an Vue 2 project some years ago. As far as I can see Valentin Rapp gave you the correct explanation already. The documentation also states:
Array Mutators
Vue is able to detect when an reactive array's mutation methods are called
Source: https://vuejs.org/guide/essentials/list.html#array-change-detection
One possible solution would be, to push your resulting elements to the reactive array, therefore triggering it's mutators.
// ...
created (){
getUsersWithPoints().then((users) => {
users.forEach(user => {
this.items.push(user)
})
console.log('Users:', this.items)
})
},
// ...
Object Mutators
Depending on the size of your users-array (> 10k) this approach could potentially lag/very slow on some devices. Another approach would be, to use the reactive mutators of an object by updating an objects prop. Please note, that I haven't used that with Vue 3 options API yet, so this could be faulty:
// ...
data() {
return {
user: {
items: [],
loaded: false, // can be used for some loading animation
},
}
},
created (){
getUsersWithPoints().then((users) => {
this.user.items = users
this.user.loaded = true
console.log('Users:', this.user.items)
})
},
// ...
Composition API
As stated before and propably lead to the confusion of Valentin Rapp is the useage of the options API in Vue 3. While this should be fully supported by Vue, with version 3 the composition API was implemented that could, besides other things, fix this problem for you:
// Vue 3 Component with Composition API — not a 1:1 approach!
// ...
setup() {
const items = ref([])
getUsersWithPoints().then((users) => {
users.forEach(user => {
this.items.push(user)
})
})
return {
items,
}
},
// ...
VS Code Saving Bug
This "bug" is more likely a confirmation for the problem stated above: mutators. Yet your given description is way to little to give you some detailed information here.
We don't know what your whole toolchain between "storing in vscode" to "bringing the new code to the browser" looks like. How does your code get transpiled? How is the file beeing watched? How is the hot reloading implemented?
Yet in general I think this will work like so:
- File gets stored within your editor
- Your toolchains watcher detects a changed file hash for one js file
- Your toolchain transpiles your code
- Your hot reload service will reload and inject the new code
- In order to sync your current data within your browser instance and the default data within the hot-reloaded component will be replaced correctly with usage of vues mutators.
- Your component therefor will detect the changes correctly. Your v-for get's rerendered and the data will be displayed correctly
But as I said, this is pure speculation and is purly depending on your setup.
CodePudding user response:
So I tried multiple things to fix it:
- Switching to Composition API
- Using Array Mutators instead of replacing the Array
- Using async - await instead of then(..)
Unfortunately, none of that fixed the problem. So I looked more deeply into the getUsersWithPoints function to find what I think was the mistake: Using forEach with an async function. Using Promise.all instead fixed the issue. Here is the code:
async function getUsersWithPoints(){
let usersExtended = new Array();
const usersQuery = query(collection(db, `groups/${wgKey}/users`));
const usersSnap = await getDocs(usersQuery);
await Promise.all(usersSnap.docs.map(async(user) => {
const tasksQuery = query(collection(db, `${user.ref.path}/tasks`));
const tasks = await getDocs(tasksQuery);
let points = 0;
tasks.forEach((task)=>{
points =task.data().points;
});
usersExtended.push({
username: user.data().name,
points: points,
id: user.id
});
}));
return usersExtended;
}
Using array mutators seemed like a logical thing to do, although the docs say:
When working with non-mutating methods, we should replace the old array with the new one ... You might think this will cause Vue to throw away the existing DOM and re-render the entire list - luckily, that is not the case. Vue implements some smart heuristics to maximize DOM element reuse, so replacing an array with another array containing overlapping objects is a very efficient operation.
source: https://vuejs.org/guide/essentials/list.html#array-change-detection
So as far as I understand, replacing the array will not re-render the entire DOM, but vue uses "smart heuristics" to render the replacing array very efficiently. So now that I fixed the data call I tried replacing and using Array Mutators and both worked.
CodePudding user response:
In your created lifecycle hook you overwrite the reactive data array. This will not trigger an update as described here:
https://vuejs.org/guide/essentials/list.html#array-change-detection
You will need to push the data to the array. To modify and clear it use splice.
In Vue 3 you will not have to deal with this anymore.