ChatGPT解决这个技术问题 Extra ChatGPT

Using $refs in a computed property

How do I access $refs inside computed? It's always undefined the first time the computed property is run.

Yes it's defined only when the first render loop is done. For what it worth, it's explicitly not recommended to use $refs inside computed properties as it is not reactive. vuejs.org/v2/guide/components.html#Child-Component-Refs You may have to find a better pattern...
Using Watch function inside of the mounted: enter link description here

k
kissu

Going to answer my own question here, I couldn't find a satisfactory answer anywhere else. Sometimes you just need access to a dom element to make some calculations. Hopefully this is helpful to others.

I had to trick Vue to update the computed property once the component was mounted.

Vue.component('my-component', {
  data(){
    return {
      isMounted: false
    }
  },
  computed:{
    property(){
      if(!this.isMounted)
        return;
      // this.$refs is available
    }
  },
  mounted(){
    this.isMounted = true;
  }
})

Had exactly the same requirement and this was the only workable solution I found. Bound the class property to a computed property and had to apply some classes if a nested component (the ref) had some properties set.
@Bondsmith Sorry haha, I'm the asker and answerer, guess I forgot to accept my own answer
I didn't realize. That's funny ツ
very helpful, thanks @EricGuan for taking the time to answer your own question to help others
While this initially worked, it's not reactive, when the refs change, it doesn't update, for instance, clientWidth is static, as if it only gets the initial size on mounted, and if the object changes, it doesn't update.
t
tony19

I think it is important to quote the Vue js guide:

$refs are only populated after the component has been rendered, and they are not reactive. It is only meant as an escape hatch for direct child manipulation - you should avoid accessing $refs from within templates or computed properties.

It is therefore not something you're supposed to do, although you can always hack your way around it.


k
kissu

If you need the $refs after an v-if you could use the updated() hook.

<div v-if="myProp"></div>
updated() {
    if (!this.myProp) return;
    /// this.$refs is available
},

neat trick! Incorporate the v-if conditionals in the computed's logic so they get registered as dependencies. Exactly the answer to a curiosity of an element with ref and toggled by v-if
or put the code to access $refs inside nextTick: this.$nextTick(() => this.$refs...)
I wish I could upvote this answer multiple times. You saved me!
t
tony19

I just came with this same problem and realized that this is the type of situation that computed properties will not work.

According to the current documentation (https://v2.vuejs.org/v2/guide/computed.html):

"[...]Instead of a computed property, we can define the same function as a method. For the end result, the two approaches are indeed exactly the same. However, the difference is that computed properties are cached based on their reactive dependencies. A computed property will only re-evaluate when some of its reactive dependencies have changed"

So, what (probably) happen in these situations is that finishing the mounted lifecycle of the component and setting the refs doesn't count as a reactive change on the dependencies of the computed property.

For example, in my case I have a button that need to be disabled when there is no selected row in my ref table. So, this code will not work:

<button :disabled="!anySelected">Test</button>

computed: {
    anySelected () {
      if (!this.$refs.table) return false

      return this.$refs.table.selected.length > 0
    }
}

What you can do is replace the computed property to a method, and that should work properly:

<button :disabled="!anySelected()">Test</button>

methods: {
    anySelected () {
      if (!this.$refs.table) return false

      return this.$refs.table.selected.length > 0
    }
}

I think this approach is the best one so far. It doesn't spread the instructions out to data(), updated(), and other parts. Clean and concise.
Methods are not reactive and won't be updated if the ref property changes.
k
kissu

For others users like me that need just pass some data to prop, I used data instead of computed

Vue.component('my-component', {
    data(){
        return {
            myProp: null
        }
    },    
    mounted(){
        this.myProp= 'hello'    
        //$refs is available              
        // this.myProp is reactive, bind will work to property
    }
})

This is better than the accepted answer if you do not need it to be reactive.
k
kissu

Use property binding if you want. :disabled prop is reactive in this case

<button :disabled="$refs.email ? $refs.email.$v.$invalid : true">Login</button>

But to check two fields i found no other way as dummy method:

<button :disabled="$refs.password ? checkIsValid($refs.email.$v.$invalid, $refs.password.$v.$invalid) : true">
  {{data.submitButton.value}}
</button>
methods: {
   checkIsValid(email, password) {
      return email || password;
   }
}

M
Marco Santana

I was in a similar situation and I fixed it with:

  data: () => {
   return { 
    foo: null,
   }, // data

And then you watch the variable:

    watch: {
     foo: function() {
       if(this.$refs)
        this.myVideo = this.$refs.webcam.$el;
       return null;
      },
    } // watch

Notice the if that evaluates the existence of this.$refs and when it changes you get your data.


J
Justo

What I did is to store the references into a data property. Then, I populate this data attribute in mounted event.

    data() {
        return {
            childComps: [] // reference to child comps
        }
    },
    methods: {
        // method to populate the data array
        getChildComponent() {
            var listComps = [];
            if (this.$refs && this.$refs.childComps) {
                this.$refs.childComps.forEach(comp => {
                    listComps.push(comp);
                });
            }
            return this.childComps = listComps;
        }
    },
    mounted() {
        // Populates only when it is mounted
        this.getChildComponent();
    },
    computed: {
        propBasedOnComps() {
            var total = 0;
            // reference not to $refs but to data childComps array
            this.childComps.forEach(comp => {
                total += comp.compPropOrMethod;
            });
            return total;
        }
    }

R
Raine Revere

Another approach is to avoid $refs completely and just subscribe to events from the child component.

It requires an explicit setter in the child component, but it is reactive and not dependent on mount timing.

Parent component:

<script>
{
  data() {
    return {
      childFoo: null,
    }
  }
}
</script>

<template>
  <div>
    <Child @foo="childFoo = $event" />

    <!-- reacts to the child foo property -->
    {{ childFoo }}

  </div>
</template>

Child component:

{
  data() {
    const data = {
      foo: null,
    }
    this.$emit('foo', data)
    return data
  },
  emits: ['foo'],
  methods: {
    setFoo(foo) {
      this.foo = foo
      this.$emit('foo', foo)
    }
  }
}

<!-- template that calls setFoo e.g. on click -->

This is the correct approach, brb