updated: function() {
console.log("updated");
this.$emit("render-vue", this.$el.offsetHeight);
},
This works fine enough...but in my app, props get updated (which I don't care about), and then there is a fetch() that updates data, resulting in additional DOM rendering.
So, that means that I get 2 'updated' events in succession! 👎🏾
Essentially, I only want updated to perform the $emit when data has been fully updated and the DOM is finished.
Currently, $emit is happening 2 times in succession, and that's not helping situation - IT DOM gets updated with props and then right after that another updated occurs when data gets updated after fetch().
Now, I could watch - tried that. The issue there is that $emit will fire off before all DOM is updated, sending the wrong info as argument.
It's almost like I need to watch a specific piece of data...and then, and only then, have updated send the $emit. 😖
For additional context, there are no children - this is a child component.
I can probably get this to work by using a setTimeout()...but come, on! That's sloppy! 👎🏾
In the code block that fetches the data, set a property to indicate new data is available
fetch(url)
.then(() => {
// whatever you're doing with the data
this.newData = true;
});
Check this state variable in the updated hook
updated() {
if (this.newData) {
this.$emit("render-vue", this.$el.offsetHeight);
this.newData = false;
}
}
Related
I have made some sandbox code of my problem here:
https://codesandbox.io/s/clever-zeh-kdff1z
<template>
<div v-if="started">
<HelloWorld :msg="msg" #exit="exit" #remake="remake" />
</div>
<button v-if="!started" #click="started = !started">start</button>
</template>
<script>
import HelloWorldVue from "./components/HelloWorld.vue";
export default {
name: "App",
components: {
HelloWorld: HelloWorldVue,
},
data() {
return {
started: false,
msg: "Hello Vue 3 in CodeSandbox!",
};
},
methods: {
exit() {
this.started = false;
},
remake() {
this.msg = this.msg + 1;
//this code should recreate our child but...
this.exit();
this.started = true;
// setTimeout(() => {
// this.started = true;
// });
},
},
};
</script>
So! We have 2 components parent and child. The idea is simple - we have a flag variable in our parent. We have a v-if statement for this - hide / show an element depend on the flag value "false" or "true". After we toggle the flag - the child component should be recreated. This is the idea. Simple.
In our parent we have a button which will set the flag variable to "true" and our child will be created and will appear on our page.
Ok. Now we have 2 buttons inside our child.
One button is "exit" which is emit an event so the flag variable of parent will set to "false" and the elemint will disappear from our page(It will be destroyed btw). Works as charm. Ok.
The second button "remake". It emit event so the flag variable will be just toggled (off then on). Simple. We set to "false", we set to "true". So the current child should dissapear, and then imediatly will be created new one.
But here we are facing the problem! Ok, current child is still here, there is no any recreation, it just updates current one... So in child I have checked our lifecycle hooks - created and unmounted via console.log function. And the second button dont trigger them. Start->Exit->Start != Start->Remake.
So can anyone please explain me why this is happening? I cant figure it out.
Interesting thing, if you can see there is some asynchronous code commented in my demo. If we set our flag to "true" inside the async function the child will be recreated and we will see the created hook message but it seems like crutch. We also can add a :key to our component and update it to force rerender, but it also seems like a crutch.
Any explanations on this topic how things work would be nice.
Vue re-uses elements and components whenever it can. It will also only rerender once per tick. The length of a 'tick' is not something you should worry yourself about too much, other than that it exists. In your case the this.exit() and this.started = true statements are executed within the same tick. The data stored in this.started is both true in the last tick and the current tick as it does not end the tick in between the statements, and so nothing happens to your component.
In general you should think in states in Vue rather than in lifecycles. Or in other words: What are the different situations this component must be able to handle and how do you switch between those states. Rather than determining what to do in which point in time. Using :key="keyName" is indeed generally a crutch, as is using import { nextTick } from 'vue'; and using that to get some cadence of states to happen, as is using a setTimeout to get some code to execute after the current tick. The nasty part of setTimeout is also that it can execute code on a component that is already destroyed. It can sometimes help with animations though.
In my experience when people try to use lifecycle hooks they would rather have something happen when one of the props change. For example when a prop id on the child component changes you want to load data from the api to populate some fields. To get this to work use an immediate watcher instead:
watch: {
id: {
handler(newId, oldId) {
this.populateFromApi(newId);
},
immediate: true
}
}
Now it will call the watcher on component creation, and call it afterwards when you pass a different id. It will also help you gracefully handle cases where the component is created with a undefined or null value in one of the props you expect. Instead of throwing an error you just render nothing until the prop is valid.
i have a button in my project when i click on it two queries added to URL
onClickBtn(){
this.$router.push({queries: {name:'kevin' , age:21} })
}
and I have a watch on $route
watch:{
$route:function(){
// call API
}
}
and when i click on that button severall time
watch calls my API every time although nothing has changed
and this make a problem form me
because nothing has changed in route But API is called and the same data is
received .
what should I do to avoid calling API in watch , when queries don't changed ??
The object you are pushing on the router is always different, that's why the $route watch is launched.
You can compare the data you receive in the watch, so that when they are different then you invoke the API:
watch:{
'$route' (newRoute, lastRoute){
// Check if query is different
// call API
}
}
On top of the answer that Cristian provided, you could also even double-check if your stuff has changed before even pushing a new object to your router.
Like this
checkIfUpdateNeeded && this.$router.push({queries: {name: 'kevin', age:21 } })
That way, you will have less moving parts and you won't have a trigger in the watcher for "nothing", especially if you're pushing a bigger object and want to make a deep-diff between 2 objects.
Using Vue2, I have a list of books on a page where the details are shown via a child component. The user can delete one of the books from within the child component. I want the list to update after the delete. I'm using an $emit from the child after the record is deleted from the database. I can see from the console that the $emit is sent with the correct new list of books in the payload. However, the callback function in the parent isn't being fired. I can tell because the console.log I have at the beginning of the callback isn't showing up in the console.
Any suggestions on what I'm doing wrong?
Here's what's in the parent:
<div v-for="book in books" :key="book.BookID" #book-refresh="refreshbooks">
<BookCard :book="book" />
</div>
Here's the callback function:
async refreshbooks(newbooks) {
console.log('in books.vue, starting refreshBooks, newbooks is: ', newbooks)
this.$set(this, "books", newbooks)
.bind(this)
},
Here's what's in the function (in the child component) that fires the $emit:
async deleteBook(bookID) {
this.dialogdelete = false
await (EventService.deleteBook(bookID, this.user.UserID))
.then((newbooks) => {
this.$emit('book-refresh', newbooks)
}
)
},
TJ, thank you for the suggestion. I did as you said and created the mre which worked. I then went back to see what was different. The only things I changed were the name(s) of the functions. I changed the book-refresh and refreshbooks all to a simple refresh. Now it works perfectly. I haven't taken the time to see if I just didn't have the names matched up properly or if there's just something about hyphenated names? I did read somewhere yesterday that mixed case makes a difference, so maybe? In any case, keeping it super simple has helped me to move on.
Thanks again for helping me see the forest instead of the trees!
I'm working on my first Vue 3 (and therefore vuex 4) app coming from a pretty solid Vue 2 background.
I have an component that is loading data via vuex actions, and then accessing the store via a returned value in setup()
setup() {
const store = useStore();
const fancy = computed(() => store.state.fancy);
return { fancy };
},
fancy here is a deeply nested object, which I think might be important.
I then want to run a function based on whenever than value changes, so I have set up a watch in my mounted() lifecycle hook
watch(
this.fancy,
(newValue) => {
for (const spreadsheet in newValue) {
console.log(Object.keys(newValue[spreadsheet].data))
setTimeout(()=>{
console.log(Object.keys(newValue[spreadsheet].data))
},500)
...
I tried using the new onMounted() hook in setup and watchEffect and store.watch instead, but I could not get watch/watchEffect to reliably trigger in the onMounted hook, and watchEffect never seemed to update based on my computed store value. If you have insight into this, that would be handy.
My real issue though is that my watcher gets called seemingly before the value is updated. For instance my code logs out [] for the first set of keys, and then a half second later a full array. If it was some kind of progressive filling in of the data, then I would have expected the watcher to be called again, with a new newValue. I have also tried. all the permutations of flush, deep, and immediate on my watcher to no avail. nextTick()s also did not work. I think this must be related to my lack of understanding in the new reactivity changes, but I'm unsure how to get around it and adding in a random delay in my app seems obviously wrong.
Thank you.
I have a component with a created method:
created() {
this.initMap();
}
The initMap is to initialize Google maps, depending on whether the URL segment corresponds to 'map' or not like so:
initMap() {
const pathname = location.pathname.replace(/\/+$/, '');
const segment = pathname.split('/').pop();
if (segment === 'map') this.showMap();
}
The above bit of code has a ShowMap method that performs a Vuex commit:
showMap() {
this.$store.commit('showMap');
}
This commit however never shows up in my Vue.js devtools(under Vuex).The components watching the Vuex store value that showMap is changing also never trigger.
If I do this however:
setTimeout(() => {
this.$store.commit('showMap');
, 100);
Everything works exactly as expected.
I tried this because the changes actually happen in my Vue.js devtools, because if I look under state I can see updated values.
The Vuex commit seems to fire too fast. Is there anything that can be done about this? Why is this happening in the first place?
I can even put a console.log() into the showMap commit and it works but it still does not get picked up in the devtools and without the setTimeout all of my watcher still don't properly trigger.
Since this was a while ago I am not 100% sure how I fixed this but I think it was by using $nextTick from Vue.js.
By waiting on nextTick you ensure that your call stack for DOM updates has been cleared. This prevents DOM updates that might rely on other parts of your DOM from firing too fast.
Obviously this is way more reliable than simply setting a setTimeout with a given number of let's say 100 milliseconds from my example because if your DOM does not update in time it might still pass this window.
If setTimeout fixed your issue I suggest trying $nextTick.
https://v2.vuejs.org/v2/api/#Vue-nextTick