Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to wait for a browser re-render? Vue.nextTick doesn't seem to cover it, and setTimeout(..., 0) is not good enough. #9200

Closed
szalapski opened this issue Dec 13, 2018 · 17 comments

Comments

@szalapski
Copy link

szalapski commented Dec 13, 2018

What problem does this feature solve?

Example in a fiddle: https://jsfiddle.net/szal/eywraw8t/500316/ - when I click Load, I never see the "Loading..." indicator.

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 synchronous 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.

My unsatisfying solution: I have to wrap the work in a setTimeout(..., 25), per seebiscuit's findings here: vuejs/vuex#1023 (comment)

This seems to reveal that the documentation on Vue.nextTick at https://vuejs.org/v2/api/#Vue-nextTick is inaccurate: "Defer the callback to be executed after the next DOM update cycle. Use it immediately after you’ve changed some data to wait for the DOM update." This seems wrong; this is a good case where the user doesn't see the DOM update at all even though Vue.nextTick was used.

Calling setTimeout(..., 25) doesn't seem very discoverable or intuitive for anyone.

What does the proposed API look like?

I'd propose either a change to Vue.nextTick to work better, or a parallel call just like Vue.nextTick, perhaps "nextRender" (maybe there's a better name?) that ensures that any DOM changes can get displayed to the user before its callback is invoked asynchronously. So then I could do something like:

methods: {
 async selectThing() {
  this.thingIsLoading = true  // this triggers a dead-simple v-show
  await this.$nextRender() // the new Vue API call; give browser a chance to paint the loading indicator
  const route = setQuery(this.$route, 'thingid', this.thingViewModel.thing.id)
  this.$router.push(route) // this triggers lots of reactivity that may take a big fraction of a second to rerender/paint
  this.thingIsLoading = false
 }
}

If others feel that instead an enhancement to the behavior of Vue.nextTick is in order, I'd prefer that.

I'd also propose changing the documentation at https://vuejs.org/v2/api/#Vue-nextTick to be more accurate and perhaps explain this limitation and a workaround to future developers.

@ghost
Copy link

ghost commented Dec 13, 2018

This method works and I think there is no
real need to create new API call.

<!DOCTYPE html>

<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Example</title>

<div id="app">
  <div v-if="isLoading" ref="hey">
    Loading...
 </div>

  <button @click="loadIt">
    Load it
  </button>

  <div v-if="isDone">
    Done
  </div>
</div>

<script src="https://unpkg.com/vue@2.5.21/dist/vue.min.js"></script>
<script>
  new Vue({
    el: "#app",

    data: {
      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 browser to rerender dom
        // by requesting some property
        // from newly created node
        let width = this.$refs.hey.width

        // wait for some time, of course
        // higher than monitor refresh rate
        await this.delay(40)

        this.isDone = true
        this.isLoading = false
      }
    }
  })
</script>

@posva
Copy link
Member

posva commented Dec 13, 2018

This is because you're frozing the browser. Wait for a requestAnimationFrame before doing the heavy work. This non related to Vue however

@posva posva closed this as completed Dec 13, 2018
@szalapski
Copy link
Author

Can you acknowledge that the documentation I cited is wrong, then?

@posva
Copy link
Member

posva commented Dec 13, 2018

I'm not even sure it's wrong, the update happens, the browser still hasn't rendered the content.
I guess you can always open a PR to propose a note about js that blocks the browser main-thread

@szalapski
Copy link
Author

szalapski commented Dec 13, 2018

What good is updating the DOM if the user can't see it?

Also, to your proposed workaround: seebiscuit on the linked issue already considered that. "The behavior is very different with requestAnimationFrame—as per the spec (well at least the was MDN documents it), since requestAnimationFrame is deisgned to invoke the callback before the DOM repaints. So, in essence, requestAnimationFrame will update the final value (eg. 'C') before the DOM has a chance to process the change from to the intermediate value ('B')."

Wouldn't it be better for nextTick to do whatever it takes to allow the browser to repaint the DOM?

@ghost
Copy link

ghost commented Dec 13, 2018

But user WILL see it. Again: when you ask the browser for some property on newly added node, it will recalculate and repaint whole DOM, before returning this property value.

@szalapski
Copy link
Author

Vlad, I don't think so. The original example is one where the user never sees it. https://jsfiddle.net/szal/eywraw8t/500316/

@ghost
Copy link

ghost commented Dec 13, 2018

I understand, but with your original example you also need not to have new API call. You just blocked the processing of main thread with CPU intensive task. It was you, not the Vue, who did not give the browser a chance to repaint screen.

@szalapski
Copy link
Author

szalapski commented Dec 14, 2018

But Vue.nextTick is supposed to overcome this, according to the documentation. It seems the only way around it is to do seebiscuit's unintuitive setTimeout(...,25) solution, or by adding other delays as you suggest. I think Vue.nextTick should be enhanced to address this scenario--it is the call that is supposed to give the browser a chance to repaint the screen.

It sounds like Vlad and Posva are saying "too bad, we expect the developer to figure out all the details under the covers and do the obscure workaround." Not a very friendly API, and it contradicts the documentation. You are building a hill of pain rather than a pit of success.

@ejarnutowski
Copy link

I'm running into the same issue. When triggered, I need to do the following:

  1. Display an "Updating data" message/overlay
  2. Process some heavy data (already in browser)
  3. Render multiple charts on the page using Highcharts
  4. Hide the "Updating data" message/overlay

I've tried Vue.nextTick():

setRange(range) {
    this.updating = true;
    Vue.nextTick(() => {
        this.updateCharts(range);
        this.updating = false;
    });
}

I've tried setTimeout with 0 ms:

setRange(range) {
    let vm = this;
    vm.updating = true;
    setTimeout(() => {
        vm.updateCharts(range);
        vm.updating = false;
    }, 0);
}

The only thing I can get to work is setTimeout with a higher ms value like 25. For example:

setRange(range) {
    let vm = this;
    vm.updating = true;
    setTimeout(() => {
        vm.updateCharts(range);
        vm.updating = false;
    }, 25);
}

The setTimeout method with 5 ms works about half of the time... seems like a race condition of some sort with this timing. I'm sure I'm missing something. Does anyone have an idea? Thank you!

@Justineo
Copy link
Member

Justineo commented Mar 1, 2019

@szalapski @ejarnutowski

To wait for a browser repaint, you need to use “double” requestAnimationFrame. (Search for “double requestanimationframe” and you'll see why.)

https://jsfiddle.net/Justineo/7oqyng2c/1/

@ejarnutowski
Copy link

Thanks @Justineo, this works perfectly. However, this seems like a hacky workaround for a common scenario - I'm a little surprised there isn't a better way of handling this. Again, I appreciate your help.

@JOGUI22
Copy link

JOGUI22 commented Jul 18, 2019

Sometimes when i'm updating large parts of the DOM, Vue.nextTick is not enough. In this cases, Justineo's solution with "double" requestAnimationFrame works most of the time.

Right now, I consider this "doubleRaf" a Vue.nextTick with asteroids. Thanks @Justineo

@twickstrom
Copy link

twickstrom commented Sep 2, 2019

Ran across this issue today. Put together a little NPM package to use the double RequestAnimationFrame method that was suggested by @Justineo . Thank you for the suggestion!

This will allow you to do: this.$forceNextTick(() => { //your code here })

https://www.npmjs.com/package/vue-force-next-tick

@fbnlsr
Copy link

fbnlsr commented Sep 5, 2019

@twickstrom your package could not have come at a better time. I've been struggling with this issue for a few days and really tried to tackle it today. The setTimeout solution is not elegant. I've been using your package and it works flawlessly. Thanks a lot for your work!

@sillycube
Copy link

Is the doubleRaf fix still working in Vue 3? I try it once but get no luck.

@9mm
Copy link

9mm commented Aug 25, 2021

@sillycube it works for me it seems

@Justineo bless you

pray-lord
46a

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants