vuejs / vue

This is the repo for Vue 2. For Vue 3, go to https://github.com/vuejs/core
http://v2.vuejs.org
MIT License
207.71k stars 33.67k forks source link

Callback refs as additional alternative to "named" refs #5987

Open mcjazzyfunky opened 7 years ago

mcjazzyfunky commented 7 years ago

What problem does this feature solve?

Currently if you want to use refs to elements (DOM elements or Vue components - see attribute/prop "ref") you have to provide a string as "ref" attribute. A callback function as "ref" attribute like in React is currently not supported in Vue. It would be great if also callback functions could be provided as "ref" attributes (especially when doing a bit more advanced stuff using the "render" function).

The callback function should be called both when the referred element is created and also when it is disposed (React for example passes null in the latter case).

It seems that this feature had already been implemented in the past but has been reverted later (I do not know for what reasons the changes have been reverted) => see: "[WIP] Support for ref callback #4807"

Thank you very much.

What does the proposed API look like?

Please see line 178 here: https://github.com/vuejs/vue/pull/4807/commits/90c6c2902b1f124093ad0d514984230194cb818e

const myRefCallback(ref, remove) {...} (where "remove" is boolean) seems to me like a better solution that the one that is used in React where in the "remove" case the ref callback function is just called with null.

sqal commented 7 years ago

FYI this feature has been dropped at the time because of this bug https://github.com/vuejs/vue/issues/4998

no1xsyzy commented 7 years ago

I am not very sure what you preferred, but i think v-bind:ref could help with most of the situations. Although using :ref with v-for will still make the $refs be Arrays and disposing a components will not destroy the Array but destroy the components inside (may result in empty array/s). Take a look at this example: https://jsfiddle.net/no1xsyzy/036scaat/4/, pay attention to the console output.

mcjazzyfunky commented 7 years ago

Thanks a lot, no1xsyzy, for that information and your demo. But the problem is a bit deeper: 'String refs' are fine - I normally use them as-is in most cases. But 'callback refs' are far more powerful. Especially for those cases where you do not use templates (let's say you develop some kind of extension library for Vue) the lack of 'callback refs' may really be a show stopper.

That's why Facebook/React have switched from 'string refs' to 'callback ref': https://facebook.github.io/react/docs/refs-and-the-dom.html#legacy-api-string-refs

Moreover in Vue it's not possible to use refs in templates for functional components (as that would require callback refs). In React that's possible: https://jsfiddle.net/ozjdyr9L

In some cases and only for non-functional component it may be possible to simulate the behavior of callback refs by using unique generated ref names and some additional logic in certain lifecycle methods - but that would be really ugly, in some cases slow and not that powerful.

The feature to allow 'ref callback' would open Vue some really interessting new possibilites.

mcjazzyfunky commented 6 years ago

I changed my mind regarding the above suggested signature of the callback function: I think it would be better to make the callback signature somehow compatible with React to make it easy for wrapper or whatsoever libraries that provide something that could be used for both React and Vue.

Instead of function myRefCallback(ref, remove) {...}

I would now suggest: function myRefCallback(ref, prevRef) {...}

Means "myRefCallback" will either be called: myRefCallback(ref, null) (on rendering) or myRefCallback(null, ref) (on removing)

React does not have that second argument in the ref callbacks, but the first argument would behave exactly like in React.

decademoon commented 6 years ago

I'm also interested in Vue implementing callback refs. They're so much more powerful in my opinion.

In my situation, as a workaround, I've resorted to storing a reference to the vnode whose component instance I want a ref for, then delegating to vnode.componentInstance to get the ref at a later time.

Unfortunately I have no way of knowing when the instance has been created/mounted, because until then vnode.componentInstance is undefined. I'm pulling the instance instead of relying on a push from the framework.

stalniy commented 6 years ago

Frankly speaking I don't think that it's a good idea. Function as a ref allows to do crazy side effect stuff. Moreover, you can create internal state using closures and you will recreate this internal state each time your render function is called. Render function of functional components are called together with parent's render function

So, eventually it may have performance issues

tangjinzhou commented 5 years ago

Maybe we can use directive to implement ref callback function. I published a plugin vue-ref, you can try it. I used it in ant-design-vue and found no problems for the time being.

jumika commented 3 years ago

I really don't know why is this issue is neglected.

bryanmylee commented 2 years ago

Frankly speaking I don't think that it's a good idea. Function as a ref allows to do crazy side effect stuff. Moreover, you can create internal state using closures and you will recreate this internal state each time your render function is called. Render function of functional components are called together with parent's render function

So, eventually it may have performance issues

That's a pretty disingenuous argument. Just because you can do bad things with the feature doesn't mean the feature is bad. You could easily create infinite update cycles with get/set refs, but that doesn't mean we don't support it.

In addition, "crazy side effect stuff" over-generalizes an entire class of necessary features. The whole point of UI libraries is to manage side effects, otherwise what's the point of building UIs? It's the reason why we need watchEffect in the first place.

bryanmylee commented 2 years ago

I'm building a library for Vue 3 and this is one of the limiting factors. I have controlled components that can receive an as-child boolean prop which causes the component to render as a fragment and "pass" the required props to its children via v-slot.

<Dialog.Content>
  ...
</Dialog.Content>
<!-- becomes -->
<div id="ally-0-content" role="dialog" aria-modal="true" aria-labelledby="ally-0-title" aria-describedby="ally-0-description" data-state="open">
  ...
</div>

<Dialog.Content as-child v-slot="props">
  <section v-bind="props">
    ...
  </section>  
</Dialog.Content>
<!-- becomes -->
<section id="ally-0-content" role="dialog" aria-modal="true" aria-labelledby="ally-0-title" aria-describedby="ally-0-description" data-state="open">
  ...
</section>

However, the controlled component sometimes needs a reference to the DOM node to implement stuff like focus trapping, custom event management, layout measurements etc. If rendering as a fragment, there's no official way to get the DOM node that's supposed to receive the props, but a callback ref passed via v-slot would work perfectly.

Workarounds

Surprisingly, when I simply pass the callback ref as a property of the slot props, the callback ref works as expected.

While this solves my issue, I'm not sure if this is intended behaviour or if it will be removed in the future by a fix.

<!-- DialogContent -->
<script setup lang="ts">
const setRef = (node: HTMLElement | null) => {
  ...
};
</script>

<template>
  <slot v-bind="{ref: setRef}"
</template>

<!-- App -->
<Dialog.Content as-child v-slot="props">
  <section v-bind="props"> <!-- the callback ref is correctly triggered here -->
    ...
  </section>  
</Dialog.Content>