8

When I have a Vue component in a .vue file with a data member isLoading: false, and a template:

<div v-show="isLoading" id="hey" ref="hey">Loading...</div> <button @click="loadIt()">Load it</button> 

And a method:

loadIt() { this.isLoading = true this.$nextTick(() => { console.log(this.$refs.hey) // ...other work here that causes other DOM changes this.isLoading = false }) } 

("Loading" here refers to loading from the in-memory store, not an AJAX request. I want to do this so that I can show a simple "loading" indicator instantly while the DOM changes that might take 0.2-0.5 second or so are occurring.)

I thought that the $nextTick function would allow both the virtual and actual DOM to update. The console log reads that the item was "shown" (removing the display: none style). However, in both Chrome and Firefox, I never see the "Loading..." indicator; the short delay happens, and the other DOM changes happen without the Loading indicator being shown.

If I use setTimeout instead of $nextTick, I will see the loading indicator, but only when the other work is sufficiently slow. If there is a delay of a few tenths of a second, the loading indicator never shows. I'd like it to appear immediately on click so that I can present a snappy GUI.

Example in a fiddle

10
  • you could try using v-if instead of v-show Commented Dec 10, 2018 at 14:20
  • Tried it; no change. Commented Dec 10, 2018 at 14:26
  • 1
    Can you demonstrate in a fiddle or snippet? Commented Dec 10, 2018 at 14:28
  • Did you try calling this.$forceUpdate() immediately after this.isLoading = true? Commented Dec 10, 2018 at 14:33
  • Fiddle posted. $forceUpdate() had no effect. Commented Dec 10, 2018 at 14:41

4 Answers 4

4
+50

According to this Github 'issue' there is some 'funky' business going on with Vue.nextTick. It appears that the nextTick function actually fires just before the DOM is about to re-render. nextTick will be fired before the DOM can show the loading div and immediately gets hidden once nextTick finishes. There are some suggetions in the Github issue but not all of them work.

The only way they got it to work is if you use setTimeout with a delay. Setting the delay to 1 millisecond won't always guarantee a DOM update, so it is suggested to use around 25 milliseconds. I know this isn't really what you wanted but it's all I could find. Hopefully you get some use out of the Github link.

JSFiddle with examples

Sign up to request clarification or add additional context in comments.

3 Comments

This answer seems to work. Thanks! jsfiddle.net/szal/huLj6kby/2
No problem. But I agree with @AlexeySh. it's not really good practice to use these 'magic numbers'. It works, I'll give you that but should there be a better alternative I'd really suggest switching to that
The issue still exists in Vue3. I also tried to $forceUpdate and it didn't work. But this setTimeout approach works like a charm.
2

I'm pretty sure you're over complicating things.

This appears to work just fine:

new Vue({ el: "#app", data: { isLoading: false, done: false }, methods: { loadIt() { this.isLoading = true // Simulate some long running AJAX request... window.setTimeout(() => { this.done = true this.isLoading = false }, 3000) } } }) 

Edit: After reading further about your issue, the code below appeared to work for me. It's kind of a hack really. I guess your issue is to do with the event loop?

new Vue({ el: "#app", data: { isLoading: false, done: false }, methods: { loadIt () { this.isLoading = true window.setTimeout(() => { for (var i = 1; i < 40000000; ++i) { this.done = !i } this.done = true this.isLoading = false }, 10) } } }) 

Note: This 100% works but I did notice that JSFiddle is a bit odd with 'saving' and 'running'. Make sure to hit Command / Ctrl + S first and then click the Run button.

4 Comments

Thanks for the idea, but that only works because of the setTimeout, and then only if the delay is long enough. I don't want to simulate an AJAX request, but rather a long-running DOM manipulation. Without a setTimeout, the indicator never shows; here's a demo of your suggestion where you can see it doesn't work: jsfiddle.net/szal/o7ykfrhd/2
And here's an example with the work inside a setTimeout that also doesn't work. jsfiddle.net/szal/yk84rbwq/4
Great thanks-yes, that is what I had to do. Seems wrong, and I asked for a fix: github.com/vuejs/vue/issues/9200 - but I've been shut down.
Glad it worked for you! Please consider marking this as the answer. Thanks :)
1

Problem lies in browsers itself. They have lazy approach to rerendering. Intentionally, to protect you from layout thrashing. So you should use a little trick to force the browser to rerender screen immediately:

loadIt() { this.isLoading = true this.$nextTick(() => { // You are right here, reference is now // available, so far, so good console.log(this.$refs.hey) // And here the problem lies. Browser is now // intentionally waiting for another changes in DOM to // batch them, to compute and rerender all changes l // So, when you change the visibility now, back to // invisible state, browser batch these two changes // and it will do it both at once. The result is your // component will be not rendered at all. So, here you // you should force the browser to rerender screen // now, immediately, with, for example, asking it for // new element width: let width = this.$refs.hey.width // Now the browser have no other option, just rerender // whole DOM and give you new element width. So, now // your component is rendered by browser for sure and // you can switch the visibility again: this.isLoading = false }) } 

Complete example:

<template> <div v-if="isLoading" ref="hey"> Loading... </div> <button @click="loadIt"> Load it </button> <div v-if="isDone"> Done </div> </template> <script> export default { name: 'MyComponent', data () { return { isLoading: false, isDone: false } }, methods: { delay: ms => new Promise(res => { setTimeout(res, ms) }), async loadIt() { this.isLoading = true // wait for vdom -> dom await this.$nextTick() // force dom rerendering console.log(this.$refs.hey.width) // wait for some time, higher than monitor refresh rate await this.delay(40) this.isDone = true this.isLoading = false } } </script> 

3 Comments

One of us is missing something, as just I tried that and it didn't help: jsfiddle.net/szal/bx9docf4/6 -- I thought your "force" code should go earlier, but that didn't help either: jsfiddle.net/szal/djw3crzu/4
I'm on smartphone now. Paste your example at jsbin.com instead, jsfiddle is not mobile friendly
Jsbin doesn't seem to be working right--the for loop in my example executes immediately regadless of the number of loops, so I can't find a way to simulate a long in-memory operation. Please take a look at the jsfiddle if you can.
-1

This is because you are using synchronous js operation

for (var i = 1; i<10000000; ++i){ this.done = !i } 

If you want to get the loading indicator to appear on the page you have to use a web worker or change logic to asynchronous style.

Working fiddle https://jsfiddle.net/508odkm9/1/

6 Comments

Isn't that what nextTick() is supposed to help with?
@PatrickSzalapski as you can see nextTick works fine - loading message is appearing in the dom (at least you can see it in the console). the browser works in an unexpected manner so you can see it in the dom but it doesn't visible on the page.
Yes, exactly--I want to get the loading indicator to appear on the page.
Thanks, I see how that works--yes, that would work if I were invoking code here. However, my problem is that the slowness happens as a result of reactivity changes elsewhere in the app--other Vue components are reacting to a change to a store value. Instead of trying to show all that logic here, I am simulating it in a dummy for loop. How do I wrap that for loop so that the "Loading..." indicator shows in the browser regardless of what comes after it?
@PatrickSzalapski the same approach. your components work synchronously
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.