vuejs / rfcs

RFCs for substantial changes / feature additions to Vue core
4.85k stars 551 forks source link

Add forwardRef API to make it possible let component handle ref itself #258

Open Jokcy opened 3 years ago

Jokcy commented 3 years ago

When we pass ref to a component or DOM element, the ref will point to component instance or DOM element which handled by Vue internally.

In some case we may want to handle ref ourself. For example, if I want wrapper a Input component but just want to handle some logic when use input. I maight do:

const Input = defineComponent({
  setup() {

    const localHandleInput = () => {}
    return () => <input onInput={localHandleInput} />
  }
})

At this case I don't want the ref passed to the component just point to the component instance because it's nothing there. I want to point the ref to the real input DOM element, by now I did not find any way to implement this.

Maybe we need to add some api like forwardRef in React, or add an forwardRef option when defineComponent. Then Vue pass the ref handler into the setup method, so that we can handle ref ourself.

const Input = defineComponent({
  forwardFef: true,
  setup(props, { ref }) {

    const localHandleInput = () => {}
    return () => <input onInput={localHandleInput} ref={ref} />
  }
})
Jokcy commented 3 years ago

https://vue3js.cn/docs/zh/guide/component-template-refs.html#%E6%A8%A1%E6%9D%BF%E5%BC%95%E7%94%A8 https://vue3js.cn/docs/zh/guide/composition-api-template-refs.html#%E6%A8%A1%E6%9D%BF%E5%BC%95%E7%94%A8

As these two documents said, I think vue ref is different from react ref.

React ref on a component could be forwarded to child element inside child component. So we can access child element by ref.

But we can not do that in vue. In parent component we could access child component by ref. And then we could call child component methods by ref. We should also define ref inside child component if we need manipulate the input element inside child component.

And I think vue ref is better than react ref. 😄

This issue is to talk about we need to add the forwardRef ability to vue, it's not talking about if vue already have this ability.

And what do you mean by "vue ref is better than react ref"?

leopiccionia commented 3 years ago

The biggest challenge about manually implementing a behavior similar to forwardRef in Vue, currently, is the fact that refs returned by setup are unwrapped inside template (docs).

However, as mentioned in vuejs/composition-api#317, you can pass a closure as a prop to work around that: https://jsfiddle.net/leopiccionia/28ghm1et/

It can even be refactored into an util function: https://jsfiddle.net/leopiccionia/s1vpu2f3/

The second biggest challenge is that you can't have a prop named ref, but the workaround is just using a different name.

However, I'm not sure if something like forwardRef is very Vue-y. I'd rather pass the raw element as payload of the re-emitted event, and access it in the event callback. https://jsfiddle.net/leopiccionia/3rofa06n/

Jokcy commented 3 years ago

The biggest challenge about manually implementing a behavior similar to forwardRef in Vue, currently, is the fact that refs returned by setup are unwrapped inside template (docs).

However, as mentioned in vuejs/composition-api#317, you can pass a closure as a prop to work around that: https://jsfiddle.net/leopiccionia/28ghm1et/

It can even be refactored into an util function: https://jsfiddle.net/leopiccionia/s1vpu2f3/

The second biggest challenge is that you can't have a prop named ref, but the workaround is just using a different name.

However, I'm not sure if something like forwardRef is very Vue-y. I'd rather pass the raw element as payload of the re-emitted event, and access it in the event callback. https://jsfiddle.net/leopiccionia/3rofa06n/

Yeah, we do will face these problems. But the forwardRef API is not about if we can get the element, it's about what behavior ref prop will do. The mostly common pattern using forwardRef in react is called HOC, for example I have an HOC to log some props when using an component:

const NewButton = logProp('type')(Button)

logProp is an HOC and it return NewButton as a component, when using NewButton it will log type prop everytime. In this example NewButton is just Button will log, but if we do not forwardRef when people put ref on NewButton, they will get notthing since it is a functional component.

As the developer of logProp, we want the ref prop can be passed to the real Button component or othe component pass to the logProp HOC. And this is what forwardRef for.

leopiccionia commented 3 years ago

Yeah, we do will face these problems. But the forwardRef API is not about if we can get the element, it's about what behavior ref prop will do. The mostly common pattern using forwardRef in react is called HOC (...)

Vue already handles the forwarding of the ref attribute in functional components, no matter the level of indirection. https://jsfiddle.net/leopiccionia/teh05k9v/

The part that Vue still doesn't support natively is letting the user manually handle the ref attribute. That's where you need the workarounds cited.

Jokcy commented 3 years ago

Yeah, we do will face these problems. But the forwardRef API is not about if we can get the element, it's about what behavior ref prop will do. The mostly common pattern using forwardRef in react is called HOC (...)

Vue already handles the forwarding of the ref attribute in functional components, no matter the level of indirection. https://jsfiddle.net/leopiccionia/teh05k9v/

The part that Vue still doesn't support natively is letting the user manually handle the ref attribute. That's where you need the workarounds cited.

Due to the limitation of functional component, it only solve part of problem.

agileago commented 2 years ago

@Jokcy maybe we can use expose to achieve it.

function useForwardRef() {
  const instance = getCurrentInstance()!
  function handleRefChange(realRef: any) {
    instance.exposed = realRef
    instance.exposeProxy = realRef
  }
  return handleRefChange
}

the example:


const A = defineComponent({
  props: {
    size: String as PropType<'small' | 'large' | 'middle'>,
  },
  setup(props, ctx) {
    const originExpose = {
      focus() {
        console.log('focus')
      },
    }
    ctx.expose(originExpose)
    return () => <div>my size is {props.size}</div>
  },
})

function createSizeA(size: 'small' | 'large' | 'middle') {
  return defineComponent((props, ctx) => {
    const handleRef = useForwardRef()
    return () => <A {...ctx.attrs} size={size} ref={handleRef}></A>
  })
}

const LargeA = createSizeA('large')

export default class HelloWorldView extends VueComponent {
  aRef = ref()

  @Hook('Mounted')
  mounted() {
    this.aRef.value?.focus()
    console.log(this.aRef.value)
  }
  render() {
    return <LargeA ref={this.aRef}></LargeA>
  }
}
DmitriiBaranov-NL commented 2 years ago

Perhaps, instead there could be both e-ref (for element ref) and c-ref (for component ref) The issue with having e-ref on a component is that a component may not have a single root component. But for such case, it's e-ref could either be bound to all the roots at once as an array or it could be forbidden on a multi-root component (Or we could introduce yet another m-ref or something for this edge case). Introducing e-ref and c-ref next to ref would keep this change backwards-compatible. I'm not sure if it worths adding this whole system all the efforts and complexity, but on the other hand, it would introduce a clear distinction between two different concepts -- element and component reference. Also, I believe it'd be better to have ref bindings rather than magic string references to ref variables by name, and they should behave as v-model:

<MyComponent e-ref="elMyComponent" c-ref="cmpMyComponent" />
<component :name="genericComponentName" e-ref="elGenericComponent" c-ref="cmpGenericComponent" />
orimay commented 2 years ago

So far I use

  <textarea
    :ref="el => emit('e-ref', el as HTMLTextAreaElement)"
    ...

and

  <CTextArea
    @e-ref="el => (elDetails = el)"
    ...
hcg1023 commented 2 years ago

I would also like to add a forwardRef method, so that UI components can be unified to expose DOM operations, or have an extra property like C-ref, but it has to be official, which would be a great help in developing a Vue library

hcg1023 commented 2 years ago

Especially when using the Script Setup syntax, we don't expose anything by default, but as a consumer, I can't expect all components to expose me a DOM

Thy3634 commented 2 years ago

I believe that child component should control itself's expose, but if there is only one root element, "custom element" may help.

There is a example to use "expose" or "custom directive" to get root element.

SFC playground link

crush2020 commented 1 year ago

I think it's good to keep the original. value writing

sadeghbarati commented 7 months ago

React will Deprecate forwardRef API

Replacement is to add ref as prop to the Component

andrey-hohlov commented 7 months ago

@sadeghbarati can you please give a proof link?

sadeghbarati commented 7 months ago

https://twitter.com/acdlite/status/1719496241501847884?t=A3qNDupM9tnJq7mRG589pg&s=19