skip to Main Content

I’m having trouble creating a reference to an element and storing it inside an (non-reactive) object that I initialize like this:

const myObj = {
  myRef: ref(null),
  ...
}

And in the template I have a similar situation:

<template>
   ...
   <Comp :ref="(el) => { myObj.myRef = el }"/>
   ...
</template>

Now, if I try to access myObj.myRef (after mounting) I get undefined
I am sure this is due to my lack of understanding of how ref and reactive are unboxed within templates. Can someone explain to me what are the values that I am actually working with within :ref="..."?

2

Answers


  1. Only top-level refs are auto-unwrapped in the template. For others (such as your one in an object) you still need to use .value.

    Can someone explain to me what are the values that I am actually working with within :ref="..."?

    These are the docs: Vue Guide – Template Refs: Function Refs.

    <Comp :ref="(el) => { myObj.myRef = el }"/>
    

    el is an element reference or component instance:

    The function receives the element reference as the first argument.

    In this case, it is a component reference (see my note at the end of this answer).

    myObj.myRef is a ref, not auto-unwrapped (because it is not top-level). Hence, the ref itself gets overwritten, not its value. If you want to use this approach, you can set the ref using myObj.myRef.value = el.

    Now, if I try to access myObj.myRef (after mounting) I get undefined.

    I’m not quite sure what’s happening here, especially since you haven’t shown this code. Accessing myObj.myRef verbatim should give you the component instance (as you overwrote the ref with the reference). However, I suspect that you are actually using .value (myObj.myRef.value), which logically yields undefined, given that a component instance has no value property by default (you could expose one, if you wanted to).


    Since you seem to be simply binding the reference to your ref, you don’t need the function syntax. You can just use the following instead:

    <Comp :ref="myObj.myRef"/>
    

    Note the fact that I am binding it now, not just passing the name (:ref vs ref). This is required because it includes a property access and is therefore not just a name.

    However, I suspect that this is a simplified example and that there is more going on than this.


    Now, in the comments you gave a full implementation and asked if could be improved.

    (As I guessed earlier, there is more detail than in your question: you are actually accessing and saving an exposed ref on component, not just saving the reference itself.)

    So, let’s see what you’re trying to accomplish:
    You appear to want to have access to a template ref internal to a component.

    Your current implementation does this by

    • creating a template ref in the component,
    • exposing it (with defineExpose),
    • creating an otherwise-plain ref in the parent,
    • binding a function to the ref attribute of the component, which saves the exposed template ref to the other ref.

    Here’s what we’ve got so far (given the implementation you linked to in the comments):

    Comp.vue

    <script setup>
    import { ref } from 'vue'
    
    const el = ref(null);
    
    defineExpose({ inner: el });
    </script>
    
    <template>
      <p ref="el">Phrase one</p>
      <p>Phrase two</p>
    </template>
    

    App.vue

    <script setup>
    import { ref } from 'vue'
    import Comp from './Comp.vue'
    
    const msg = {
      ref: ref(null)
    }
    
    function logMe() {
      console.log(msg.ref.value);
    }
    </script>
    
    <template>
      <button @click="logMe">Log the First one</button>
      <Comp :ref="(el) => { msg.ref.value = el ? el.inner : undefined }" />
    </template>
    

    Playground 1

    The component is fine, but the parent can be simplified quite a lot by using a computed ref instead of a function ref binding.

    The changes:

    • Import computed:

      import { ref, computed } from 'vue'
      
    • Create a template ref for the component itself:

      const comp = ref(null)
      

      You can use whatever name you wish, as long as you use the same one in the template.

    • Change the ref property of msg from a plain ref to a computed:

      computed(() => comp.value?.inner ?? null)
      

      This creates a computed ref where the value will be the exposed ref inner of the component if present, else null.

    • Then we simply bind the template ref for the component to the component:

      <Comp ref="comp" />
      

    These changes yield this:

    <script setup>
    import { ref, computed } from 'vue'
    import Comp from './Comp.vue'
    
    const comp = ref(null)
    
    const msg = {
      ref: computed(() => comp.value?.inner ?? null)
    }
    
    function logMe() {
      console.log(msg.ref.value);
    }
    </script>
    
    <template>
      <button @click="logMe">Log the First one</button>
      <Comp ref="comp" />
    </template>
    

    Playground 2

    Although this is actually two lines longer (the extra ref and blank line), it is much simpler. Instead of mutating an object in a function handler for a ref binding, there is a just a computed ref accessing the exposed property.

    This is of course all rather subjective. You may prefer the function binding, or the function approach but extracted to a method. Etc.


    Note that a template ref for a component is that component’s component instance:

    ref can also be used on a child component. In this case the reference will be that of a component instance.

    I’m not sure what you’re actually trying to achieve, but I felt that this was worth mentioning.

    Login or Signup to reply.
  2. Only top-level refs are auto-magically unwrapped inside templates.

    Your ref function will be called and will replace your ref altogether on mount. Trying to access .value on that would in all likelihood result in undefined.

    If you want the behavior you described, you’d want :ref="(el) => { myObj.myRef.value = el }".

    You can use the binding syntax, as others have suggested: :ref="myObj.myRef".

    DOM refs are a bit tricky to work with, so I’ll recommend an alternative method (that I personnaly use):

    • Have a top-level ref for each DOM element you want to reference, then you’ll be able to use names directly and have all the advantages that come with that
    • Group them in an object later if you so desire
    • Potentially use reactive around that object to have something nicer to work with

    That would give you:

    const compRef = ref();
    
    const obj = reactive({
      // [...]
      compRef,
      // [...]
    });
    
    <Comp ref="compRef" />
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search