Search code examples
javascriptvue.jssettimeoutlifecycle

this.$refs or document are not visible in mounted() unless I use setTimeout


I'm having a weird issue. I thought that $refs were available from mounted() life-cycle

But if I try to log directly it's the object i get undefined:

mounted() {
    // logs undefined
    console.log(
      this.$refs.tabsMenu
    ) 
}

And after waiting 1ms it's defined and I can get the object

mounted() {
    setTimeout(() => {
      // logs the object
      console.log(
          this.$refs.tabsMenu
      )
    }, 1) // <-- just 1ms!
}

Any thoughts about this?

My (simplified) template looks like this

<template>
  <div>
    <baseContainer>
      <ul ref="tabsMenu" id="tabs-menu" class="flex-inline flex w-full">
        <li>Home</li>
        <!-- many more items -->
        <li>Contact</li>
      </ul>
     </baseContainer>
  </div>
</div>

Solution

  • That is because mounted() does not guarantee that the DOM has finished rendering. You will need to wait for this.$nextTick():

    mounted() {
        this.$nextTick(() => {
            console.log(this.$refs.tabsMenu) 
        });
    }
    

    The reason why setTimeout(callback, 1) works for you is because you are essentially deferring the callback (which is in your code, accessing the reference and logging it to console) to the end of the callstack, which is when the DOM would've finished rendering.

    If you're familiar with using async/await, you can also do this:

    async mounted() {
        await this.$nextTick();
        console.log(this.$refs.tabsMenu);
    }
    

    Update: It seems like your ref element is actually contained in another VueJS component <baseComponent>. In this case, listening to this.$nextTick() on the parent/consuming component is not sufficient, because it doesn't guarantee the inner component is mounted and rendered.

    If you still need to use this approach, then my only advice is to ensure that the inner <baseComponent> emits some kind of event when it is mounted and its DOM has been rendered, e.g. by emitting a ready event:

    // Inside baseComponent
    mounted() {
        this.$nextTick(() => {
            this.$emit('ready');
        });
    }
    

    Then, in your parent component, you can listen to the event as such:

    <baseContainer v-on:ready="onReady">
      <ul ref="tabsMenu" id="tabs-menu" class="flex-inline flex w-full">
        <li>Home</li>
        <!-- many more items -->
        <li>Contact</li>
      </ul>
    </baseContainer>
    

    Then in the component's code:

    methods {
        onReady: function() {
            console.log(this.$refs.tabsMenu);
        }
    }