Forward refs / Support getPublicInstance #4213

Open
cpojer opened this Issue Jun 24, 2015 · 20 comments

Projects

None yet

6 participants

@cpojer
Member
cpojer commented Jun 24, 2015

For 0.14 we'd like a feature that allows us to forward refs or define what the public instance of a component should be. That way we can make higher order components that are completely transparent.

@spicyj
Member
spicyj commented Jun 24, 2015

@sebmarkbage I told @cpojer this probably wouldn't be hard to implement if we can agree on the semantics. Does findDOMNode (etc) work on the ref you get if you implement getPublicInstance? If not, this might be really easy.

@sebmarkbage
Member

Rant: Yea I've been thinking about this. This feature smells really wrong to me, since the whole point of moving refs off props was to make them an outside-concept that the framework controlled. It seems like if you want to control it, we should just make them props. That way you have the ability to fully control them and the timing. However, you don't always want to have to resolve them yourself. Also, you don't want to accidentally transfer a ref by using the property spread. Although if refs finally become less common, they should probably become manually invoked functions. So I'm torn.

Non-rant: I'm not sure the getPublicInstance API is going to be sufficient for other use cases. It doesn't provide the capability to refire.

class Container {
  state = { toggle: false };
  tick = () => {
    this.setState({ toggle: !this.state.toggle });
  }
  componentDidMount() {
    setInterval(this.tick, 100);
  }
  render() {
    return <div>{this.state.toggle ? this.props.children : <Pause />}</div>;
  }
}

class Wrapper {
  getPublicInstance() {
    return this.refs.myComponent;
  }
  render() {
    return <Container><Component ref="myComponent" /></Container>;
  }
}

In this example, there is nothing that binds the unmount/remount scenario to the Wrapper. We can't recall getPublicInstance to get the fresh component. We will keep having a stale version.

Something like this might be better:

class Wrapper {
  render() {
    return <Container><Component ref={this.ref} /></Container>;
  }
}

However, how do we know that a ref was transferred? Maybe if the function fires before this component's didMount, but that is very subtle and breaks down for the case above where it doesn't fire until later.

Maybe we add something like getControlledRef but that's an extra API and would be an side-effectful which is not ideal.

Suggestions?

@jimfb
Contributor
jimfb commented Jun 24, 2015

Personally, I'm a fan of refs becoming manually invoked functions, and so that probably influences my thinking/solution...

Make component manually invoke the ref, wrappers can forward the ref. For now, call it myref to avoid the autoinvoking that React does. Then the wrapper becomes:

class Wrapper {
  render() {
    return <Container><Component myref={this.props.myref} /></Container>;
  }
}

It's not "completely transparent", since the base component must expose a manually invoked ref instead of relying on the one provided automatically by React, but assuming you're in control of the base component, this works, right? Or maybe I'm totally miss-understanding the request :P.

@sebmarkbage
Member

The request is to provide a seamless API that is natural to use with React so that HoC can be unobservable.

@jimfb
Contributor
jimfb commented Jun 24, 2015

If we care about it being unobservable: Could we have a flag on the class that says "I'm a high level component, don't resolve my ref, I'll manually invoke it and/or pass it to someone else"? That would make the high level components completely transparent and also enable a clean migration path for making all refs manually invoked (if we decide to do that someday; migration path is that every component that takes in a ref must have that flag set).

So the new code looks like:

class Wrapper {
  render() {
    return <Container><Component ref={this.props.ref} /></Container>;
  }
}
Wrapper.manualRefs = true;
@sebmarkbage
Member

We already use static flags for things like context types etc. We might want to use flag to indicate that a component's state is pure too. That seems natural. Could potentially work for stateless functions too.

However, that brings up an interesting point. Should stateless functions automatically transfer the ref since they don't have instances?

@jimfb
Contributor
jimfb commented Jun 25, 2015

My vote would be to have refs be illegal on stateless functions, since there is no component for them to reference. That allows us to punt on the decision until a future date. It also gives us a reasonable opportunity to warn if someone tries to attach a ref to a stateless function (since, in all likelihood, the implementor of the stateless function will have forgotten to properly forward/handle the ref, which will lead to user confusion unless we can reject at the time the ref is attached thereby sidestepping the issue).

Making refs illegal on stateless functions won't result in lost functionality, since a user can always regain any potentially lost functionality by wrapping the stateless function in a HOC, thereby regaining the functionality including the ability to attach a ref and use findDOMNode.

@spicyj
Member
spicyj commented Jun 25, 2015

My vote would be to have refs be illegal on stateless functions, since there is no component for them to reference.

Seems like a rather arbitrary restriction to me. findDOMNode is as useful on a stateless function as it is on any other component. (I'm fine with introducing a way to restrict use of findDOMNode but this seems unrelated.)

@sebmarkbage
Member

The use case for stateless functions is basically to A) provide syntax sugar for another component and/or B) lock down the API by excluding configuration of some props.

For A it would be a nice convenience to forward the ref, but for B it would be detrimental to locking down the API. So I don't know. In cases like that I tend to error on the side of the easiest to change from, which is the more restrictive form - i.e. making refs not work and possibly warn on these.

@jimfb
Contributor
jimfb commented Jun 25, 2015

👍 Sounds like we've got a game plan here.

@spicyj spicyj added the big picture label Oct 11, 2015
@gaearon
Member
gaearon commented Oct 12, 2015

Just bumped into this a few days ago. React now warns when we put a ref onto a stateless component, but I do it from HOC because the user might want to have a ref: #4943 (comment)

I'm thinking of introducing a prop like wrappedRef to my HOC and doing something like return <WrappedComponent ref={this.props.wrappedRef}.

@sebmarkbage
Member

...or just use a class for your HoC.

@gaearon
Member
gaearon commented Oct 12, 2015

@sebmarkbage

My HOC is a class, the problem is when the consumer wraps a stateless component.

Pseudocode:

// my library
function connect(WrappedComponent) {
  return class Connected {
    getWrappedInstance() { // people really asked for this method
     return this.refs.instance;
    }

    render() {
      return <WrappedComponent ref='instance' {...this.props} />
    }
  }
}

// user code
connect(class Stuff { }) // good
connect(function StatelessStuff() { }) // emits warning although technically ref isn't even used
@sebmarkbage
Member

Ah, yes. That's the same problem that @yungsters has.

What methods are you expecting to forward on that ref? If you use a heuristic such as scanning the prototype, then if there is nothing on the prototype then you don't need to attach a ref because you don't have any methods to wrap.

@sebmarkbage
Member

Oh I see, I didn't read properly. You use getWrappedInstance() instead of seamlessly exposing the methods like Relay does. In that case, it's not a seamless HoC anyway.

@gaearon
Member
gaearon commented Oct 12, 2015

I didn't know Relay forwards instance methods. I'll take a look, thanks!

@cpojer
Member
cpojer commented Oct 13, 2015

Relay doesn't forward instance methods in open source. We did this internally so the mixin -> container codemod would be safe. We didn't want to support this pattern externally however and we are hoping for a resolution to this issue inside of React.

@gaearon
Member
gaearon commented Oct 15, 2015

I solved my problem by making the ref opt-in on the HOC.
reactjs/react-redux@2d3d0be

@gaearon
Member
gaearon commented Jun 6, 2016

Posted some thoughts on this in #6974.

@slorber
Contributor
slorber commented Jul 16, 2016

Hey,

Just want to say that also got this problem when using ReactIntl. Like Redux connect() it did provide a {withRef: true} option to pass to HOC: https://github.com/yahoo/react-intl/wiki/API#injectintl

Sometimes we have to espace from declarative programming for some reasons and provide component imperative API (in my case an image cropper component). It's quite annoying when someone just add some component i18n that it actually leads to a serious bug :)

@ghengeveld ghengeveld added a commit to ghengeveld/react-isomorphic-form that referenced this issue Aug 31, 2016
@ghengeveld ghengeveld Remove options to pass ref prop, because it was never supposed to be …
…possible, but it might in the future. See facebook/react#4213
094b5ec
@CMTegner CMTegner added a commit to reactjs/react-autocomplete that referenced this issue Sep 3, 2016
@CMTegner CMTegner WIP public imperative API
It's either this or `props.inputRef`, unless we want to wait for
facebook/react#4213
6c44944
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment