Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref-forwarding API #30

Merged
merged 12 commits into from
Mar 17, 2018
Merged

ref-forwarding API #30

merged 12 commits into from
Mar 17, 2018

Conversation

bvaughn
Copy link
Collaborator

@bvaughn bvaughn commented Mar 7, 2018

@trueadm
Copy link
Member

trueadm commented Mar 7, 2018

I really like this approach. Makes a lot of sense for existing applications and should be something fairly simple to build into the compiler to handle too.

@acdlite
Copy link
Member

acdlite commented Mar 7, 2018

Maybe this is out of scope, but could be worth mentioning that this same strategy can be used for all HOCs. For example:

function ThemedButton(props, theme) {
  return <button className={'theme-' + theme} {...props} />
}

export default withTheme(ThemedButton);

where withTheme internally uses a context consumer.

@streamich
Copy link

with* naming is usually used for HOCs, I'm not sure if (props, ref) => {...} is a component signature, unless old context is replaced by ref.

Also, all the new proposed APIs: createContext(), createState(), and createLifecycleEvents() seem to be FaCC factories, but this one is not and is not really a HOC either.

For example, the new context API is a FaCC factory:

const {Consumer} = React.createContext('theme');

<Consumer>{theme => <App />}</Consumer>

But if the context API would follow the pattern proposed in this RFC, it would look something like this:

const {withContext} = React.createContext('theme');

withContext(theme => <App />);

Does the React.withRef() function create a React element? If it does, it could simply be a FaCC:

<React.Ref>{(props, ref) =>
  <LegacyRelayContextConsumer {...props} forwardedRef={ref} />
}</React.Ref>

Or would <React.Ref> create an extra React element, whichReact.withRef() does not? In that case, maybe we should not create those extra elements in other APIs as well?

@acdlite
Copy link
Member

acdlite commented Mar 8, 2018

@streamich This is an HOC, except it only works for functional components. The difference is that instead of injecting additional props, it passes additional arguments.

The larger idea behind this proposal is that we could use the same pattern for other APIs, too. Like, say, context:

const ThemeContext = React.createContext('light');

function ThemedButton(props, theme) {
  return <button className={'theme-' + theme} {...props} />;
}

export default ThemeContext.withContext(ThemedButton);

@acdlite
Copy link
Member

acdlite commented Mar 8, 2018

@sebmarkbage suggested we use the use prefix instead of with since some HOCs may add more than one argument:

function Counter(props, count, updateCount) {
  return (
    <>
      Count: {count}
      <button onClick={() => updateCount(c => c + 1)}>+</button>
    </>
  );
}

export default useState(Counter, 0);

Or no arguments at all:

function ComputationallyExpensiveComponent(props) {
  const thing = somethingThatTakesALongTime(props);
  return <Whatever thing={thing} />;
}

const MemoizedComponent = usePure(ComputationallyExpensiveComponent);

@bvaughn
Copy link
Collaborator Author

bvaughn commented Mar 8, 2018

Thanks @trueadm!

Maybe this is out of scope, but could be worth mentioning that this same strategy can be used for all HOCs.

Yeah, this was what I was thinking, but maybe didn't make it clear enough. I'll try to make it clearer in the code example.

Sorry, I misread this message initially. I still agree with the sentiment of your comment, but I think mentioning it might be a bit out of scope for this RFC. 😄

@sebmarkbage suggested we use the use prefix instead of with since some HOCs may add more than one argument

@sebmarkbage always has good ideas. 😄I'll update!

Copy link
Member

@mxstbr mxstbr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would work 100% for styled-components. innerRef is one of the uglier patches of our API and I'd love to get rid of it and make the wrapper components truly transparent and pass-through.

/cc @kitten @probablyup

```

Unfortunately, within the current limitations of React, there is no (transparent) way for a
a `ref` to be attached to the above `ThemedComponent`. Common workarounds typically use a special prop (e.g. `componentRef`) like so:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use innerRef for styled-components so users can get access to the underlying DOM node, see https://www.styled-components.com/docs/advanced#refs

@jacobwgillespie
Copy link

How does this handle static class properties? It would seem to swallow them, if you did something like:

class DataContainer extends Component {
  static fragments = {widget: gql`...`}
}

export default React.useRef((props, ref) => (
  <DataContainer {...props} forwardedRef={ref} />
));

Perhaps the solution / challenge is the same as other higher-order components, in that it would either require buy-in on the original component itself so that it attached static properties to the wrapper or would require a workaround like hoist-non-react-statics.

That said, it would be nice if however this worked did not require knowledge of static properties, especially in the case where you want to wrap a third-party component with useRef without needing to know the full implementation details of the wrapped component.

This very well may be a non-issue.

@bvaughn
Copy link
Collaborator Author

bvaughn commented Mar 8, 2018

This would work 100% for styled-components.

That's great to hear, Max!

How does this handle static class properties?

Perhaps the solution / challenge is the same as other higher-order components, in that it would either require buy-in on the original component itself so that it attached static properties to the wrapper

I don't think static properties are a problem here (but please correct me if I'm overlooking something). I think your observation about attaching/forwarding them to the exported ref-type is a reasonable solution.

The key part is that you would be able to do this in a way that's transparent to users of the thing you'r wrapping. (That isn't currently possible, due to refs.)

@dantman
Copy link

dantman commented Mar 8, 2018

Perhaps the solution / challenge is the same as other higher-order components, in that it would either require buy-in on the original component itself so that it attached static properties to the wrapper or would require a workaround like hoist-non-react-statics.

I'd be wary of this at the library level. When I write a HOC I actually don't use hoist-non-react-statics, I use hoist-non-react-methods because often when wrapping a component in a HOC not only do statics get shadowed but also instance methods like focus/show/hide and React Native's measure.

So if we create something at the library level that shadows static properties and the library has to workaround it, I'm worried that if no-one remembers to take them into account we'll end up with something baked into the library (where we can't just switch to a different implementation) that handles statics but forgets about instance methods on the prototype.

@elado
Copy link

elado commented Mar 8, 2018

Thanks for writing this!

I'm having a similar issue where I have some HOCs over an and need to expose it's focus() method. it's only possible with some custom innerRef prop and a recursive getInnerMostRef helper. But this will work only if my HOCs are in my control and know how to keep track of inner refs.

That's also why I wrote https://github.com/elado/hoist-non-react-methods

Unrelated to this RFC so hiding this to reduce noise Not really related, but if there's any API change suggested, could React specific props be more distinct?

e.g.

<Comp $ref={...} $key={...} />
<Comp react-ref={...} react-key={...} />
<Comp react.ref={...} react.key={...} />

this will allow more explicit way to define react props and prevent breaking changes when new react-specific props are added. Also will allow proper spread of objects that may contain ref or key attributes.

@quantizor
Copy link

@elado XHTML does have a concept of namespaces, could do something like react:key, etc.

@mxstbr
Copy link
Member

mxstbr commented Mar 8, 2018

Namespaces in JSX have been discussed before: facebook/jsx#81 We would love to have them for styled-components so we can remove the tag whitelist but it doesn't seem like that's happening.

@bvaughn
Copy link
Collaborator Author

bvaughn commented Mar 8, 2018

I'd be wary of this at the library level. When I write a HOC I actually don't use hoist-non-react-statics, I use hoist-non-react-methods because often when wrapping a component in a HOC not only do statics get shadowed but also instance methods like focus/show/hide and React Native's measure.

I might be misunderstanding your concern, but this ref-forwarding proposal is specifically intended to help with the shadowing of instance methods like focus/show/hide. If you decorate/wrap a component with those methods, so long as you forward the ref to it, then the owner of the ref can use those methods (e.g. ref.value.focus()).

So if we create something at the library level that shadows static properties and the library has to workaround it

I don't think static properties is a common concern. Maybe I'm wrong. But if this is a case for a given component, there is a workaround (discussed above).

WRT namespaced attributes, that's outside of the scope of this RFC. 😄

@dantman
Copy link

dantman commented Mar 8, 2018

I might be misunderstanding your concern, but this ref-forwarding proposal is specifically intended to help with the shadowing of instance methods like focus/show/hide. If you decorate/wrap a component with those methods, so long as you forward the ref to it, then the owner of the ref can use those methods (e.g. ref.value.focus()).

True, I could just forward the ref to the top-level component. But I am worried that: A) sometimes I want to forward refs for something deep but I still want to offer direct methods on my component for the top level component (Say a <PromptDialog> that forwards a ref to the text input for inputRef.focus() but directly exposes it's own show/hide instead of forwarding a dialogRef to the <Dialog> it happens to use internally). B) Sometimes I may have a good reason to expose an instance method as part of my Component's API, after all here and there even React Native has reasons to do so. And I don't believe forwarding refs help that, but it sounded like they interfere with hoisting if it turns out I need to forward refs for one of my component's children.

@bvaughn
Copy link
Collaborator Author

bvaughn commented Mar 8, 2018

sometimes I want to forward refs for something deep but I still want to offer direct methods on my component for the top level component

This comment doesn't make sense to me.

I think the ref should always point to the conceptual "top-level component". So in the case of a Button component that's wrapped by a withTheme or withLocalization HOC, the ref attribute would point to the Button component. If you want to get a ref pointer to one of the things rendered by Button (e.g. an icon) then this would probably better be done by way of a custom prop (e.g. prop.iconRef).

In the case you describe, the top-level component sounds like it would be the PromptDialog, which may look something like this:

class PromptDialog extends React.Component {
  inputRef = React.createRef();

  show() {
    // Show...
  }

  hide() {
    // Hide...
  }

  focus() {
    if (this.inputRef.value != null) {
      this.inputRef.value.focus();
    }
  }

  render() {
    return isShown ? <input ref={this.inputRef} /> : null;
  }
}

External users of your dialog would use it like so:

class Example extends React.Component {
  dialogRef = React.createRef();

  componentDidMount() {
    this.dialogRef.value.show();
    this.dialogRef.value.focus();
  }

  render() {
    return <PromptDialog ref={dialogRef} />;
  }
}

It doesn't make sense for a single ref to point to multiple things. If you want to be able to have multiple refs, you still could:

class PromptDialog extends React.Component {
  show() {
    // Show...
  }

  hide() {
    // Hide...
  }

  render() {
    // Forward props.inputRef so it points to the inner <input>
    return isShown ? <input ref={this.props.inputRef} /> : null;
  }
}

function wrapForSomeReason(Component) {
  function WrapperComponent(props) {
    // (This wrapper intentionally left blank to simplify the example.)
    return <Component {...props} ref={props.forwardedRef} />;
  }

  // Forward top-level `ref` so it points to Component
  return React.useRef((props, ref) => (
    <WrapperComponent {...props} forwardedRef={ref} />
  ));
}

// Top-level `ref` will point to PromptDialog
const WrappedDialog = wrapForSomeReason(PromptDialog);

Sometimes I may have a good reason to expose an instance method as part of my Component's API, after all here and there even React Native has reasons to do so. And I don't believe forwarding refs help that, but it sounded like they interfere with hoisting if it turns out I need to forward refs for one of my component's children.

I don't really understand this comment, which makes me wonder if you're thinking of this proposal as something fundamentally different than I am. (Or maybe I'm just...misunderstanding your comment.) 😄

Do my above examples clarify any? If not, could you write a small example component that illustrates your point? I don't see how this proposal complicates or prevents you from doing what you're describing.

@dantman
Copy link

dantman commented Mar 8, 2018

@bvaughn Sorry, I read the RFC but I'll admit I found it hard to understand. Your last example makes sense and fits. I was just going off another comment talking about the interactions with hoisting and I worried because HOC usage is fairly common for me. For wrapForSomeReason I often have a bunch of reason to use some HOC, a theme context providing HOC, an API providing HOC, etc. And some of them are used on something like the PromptDialog that may be a component provided for other components to use and have a reason to have instance methods.

@bvaughn
Copy link
Collaborator Author

bvaughn commented Mar 8, 2018

No problem. If you have suggestions for ways I can clarify the RFC wording, let me know.

This RFC is intended to help with the specific case you're describing - so that you can wrap your PromptDialog with as many HOCs as you need to, without users of PromptDialog needing to know or care (because ref would just get passed through to your PromptDialog).

@jamesreggio
Copy link

jamesreggio commented Mar 15, 2018

Forgive me if this has already been covered — did a quick read-through, and didn't see anything.

I just want to confirm that the ref supplied as the callback argument within React.forwardRef is of the RefObject<T> type returned by React.createRef().

For reference, RefObject<T> has the following signature:

type RefObject<T> = {
  value: T | null
};

In some cases, its necessary for a wrapped component to both forward a ref and access it itself. I'm looking for assurance that the following code would work:

class FancyTextbox extends Component {
  state = {
    value: '',
  };

  // In this contrived method, I'd like to be able to access
  // the same <input> ref that I'm forwarding.
  reset() {
    this.setState({ value: '' }, () => {
      if (this.props.forwardedRef.value) {
        this.props.forwardedRef.value.focus();
      }
    });
  }

  render() {
    return (
      <div>
        <input
          type="text"
          ref={this.props.forwardedRef}
          value={this.state.value}
          onChange={(value) => this.setState({ value })}
        />
      </div>
    );
  }
}

export default React.useRef((props, ref) => (
  <FancyTextbox {...props} forwardedRef={ref} />
));

The above example is exceedingly contrived, but the actual usage I have in mind concerns binding native Animated events in React Native, which requires the use of an imperative API that needs to be invoked every time the ref changes. (Such code is best tucked away in a HOC, hence the need for both ref forwarding and internal ref access.)

@bvaughn
Copy link
Collaborator Author

bvaughn commented Mar 15, 2018

I just want to confirm that the ref supplied as the callback argument within React.forwardRef is of the RefObject<T> type returned by React.createRef().

No. The ref argument could also be a callback ref.

In your above example, you would have to do something (admittedly a bit awkward) like this:

class FancyTextbox extends Component {
  state = {
    value: ""
  };

  reset() {
    this.setState({ value: "" }, () => {
      if (this.ref) {
        this.ref.focus();
      }
    });
  }

  render() {
    return (
      <div>
        <input ref={setRef} />
      </div>
    );
  }

  setRef = ref => {
    this.ref = ref;

    const { forwardedRef } = this.props;
    if (typeof forwardedRef === "function") {
      forwardedRef(ref);
    } else {
      forwardedRef.current = ref;
    }
  };
}

@jamesreggio
Copy link

jamesreggio commented Mar 15, 2018

Why is setting .current legal? That's not part of the createRef() spec, right?

How resistant would you be to normalizing the type of ref to either a callback ref or RefObject<T> (preferably the latter, to encourage further adoption of createRef() and leave open the option for internal optimizations around RefObject)?

If this API is borne of a wish to simplify and normalize nested ref handling, it'd be a shame if it ended up trading old boilerplate for new (i.e., trading manual, proprietary ref unwrapping with the code above, which will need to be aware of all possibly-legal ref values, and is thus not future proof).

I feel like this RFC + the createRef() RFC gets us so close to avoiding all this unnecessary ref munging.

@dantman
Copy link

dantman commented Mar 15, 2018

Forcing refs to be a RefObject<T> would mean complex use cases that we tell people to use callback refs for would not work with this RFC.

Rather than forcing a single type of ref, if it's important for user code be able to essentially set a ref, we should expose a helper for that instead of restricting to a single type.

i.e. A React.???(ref:RefObject<T>|CallbackRef<T>, T) that would either set the ref object's value or call the callback ref.

@jamesreggio
Copy link

I'm perfectly fine with @dantman's approach, too.

I missed the boat on the createRef() RFC, but I think it was an oversight for the RefObject<T> to not contain a setValue method.

Regarding the use cases served by a callback ref, they can still be accomplished via a RefObject<T> (albeit in a convoluted manner) if you work off the assumption that componentDidUpdate will always be invoked after a change in ref.

class Wrapper extends Component {

  lastRef = this.props.wrappedRef.value;

  componentDidUpdate() {
    const { lastRef } = this;
    const { value: nextRef } = this.props.wrappedRef.value;

    if (lastRef !== nextRef) {
      // Do whatever you would've done in your callback ref.
      this.lastRef = nextRef;
    }
  }

  // ...
}

// ref is of type RefObject<T>
export default React.useRef((props, ref) => (
  <Wrapper {...props} forwardedRef={ref} />
));

@bvaughn
Copy link
Collaborator Author

bvaughn commented Mar 16, 2018

Why is setting .current legal? That's not part of the createRef() spec, right?

createRef has been updated (in alpha 2) to use .current instead of .value because the team had concerns that cases like inputRef.value.value would be confusing.

The updated example by @jamesreggio does not handle the callback ref case, so it would not work. Setting that aside, using the property initializer case for extracting a createRef ref wouldn't work either, as the ref wouldn't be available until after the component mounted.

@gaearon
Copy link
Member

gaearon commented Mar 16, 2018

I agree it would be nice to have a built-in bulletproof to compose refs (the result would always have to be a functional ref). I don’t feel like it has to be a part of this proposal though. It is also not a fundamentally new capability so perhaps it could live outside of React (I know at least one library that does this although it’ll need to be updated for object refs.) Maybe a separate RFC?

@jamesreggio
Copy link

The updated example by @jamesreggio does not handle the callback ref case, so it would not work.

The point of this example was to show how to attain the flexibility of a callback ref if React.forwardRef only provided instances of RefObject<T>. It hinges upon that assumption.

createRef has been updated (in alpha 2) to use .current instead of .value because the team had concerns that cases like inputRef.value.value would be confusing.

Name change aside, it sounds like you're saying that it's safe and supported to write to the RefObject<T>.current property. 👍


I'm fine punting the normalization concerns to user-space or a separate RFC, but in order to do so, I want to explicitly confirm what has been implicit to this conversation. Specifically:

The ref value supplied by React.forwardRef to the callback is the exact value of ref passed into the outer component returned by React.forwardRef.

This guarantee means that we can assume that the value of ref is one of the known, valid ref formats, and we can behave accordingly (e.g., by invoking callback refs or by updating .current on boxed refs).

(I feel like there may be certain internal implementation conveniences or optimizations that could be leveraged if the ref provided by the forwardRef function were opaque, but those would have to be deferred to a future major release.)

@gaearon
Copy link
Member

gaearon commented Mar 16, 2018

it sounds like you're saying that it's safe and supported to write to the RefObject.current property

Yes. (When you want to manage it manually, such as the case when composing refs.)

The ref value supplied by React.forwardRef to the callback is the exact value of ref passed into the outer component returned by React.forwardRef.

Yes. (Intentionally because always “normalizing” it to some form might be an unnecessary cost in most cases.)

@bvaughn
Copy link
Collaborator Author

bvaughn commented Mar 16, 2018

As I think about the use-case you raise, @jamesreggio, I find myself warming up to the idea of a new package that would help compose both ref-types. I'll add it to my list of things to write an RFC for (unless you beat me to it).

Edit I can't promise this means that we'll actually release this, just that we'll give it consideration. 😄

Copy link

@ivikash ivikash left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo


I recently began contributing to [`react-relay` modern](https://github.com/facebook/relay/tree/master/packages/react-relay/modern) in order to prepare it for the [upcoming React async-rendering feature](https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html).

One of the first challenges I encountered was that `react-relay` depends on the legacy, unstable context API, and values passed through legacy `context` aren't accessible in the new, [static `getDerivedStateFromProps` lifecycle](https://github.com/reactjs/rfcs/blob/master/text/0006-static-lifecycle-methods.md#static-getderivedstatefrompropsnextprops-props-prevstate-state-shapestate--null). The long-term plan for `react-relay` is to use the [new and improved contex API](https://github.com/reactjs/rfcs/blob/master/text/0002-new-version-of-context.md) but this can't be done without dropping support for older versions of React- (something `react-relay` may not be able to do yet).
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new and improved contex API -> new and improved context API


1. Add a new class method, e.g. `getPublicInstance` or `getWrappedInstance` that could be used to get the inner ref. (Some drawbacks listed [here](https://github.com/facebook/react/issues/4213#issuecomment-115019321).)

2. Specify a ["high-level" flag on the component](https://github.com/facebook/react/issues/4213#issuecomment-115048260) that instructs React to forward refs past it. This appraoch could enable refs to be forwarded one level (to the immediate child) but would not enable forwarding to deeper child, e.g.:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

appraoch -> approach

}
```

3. [Automatically forward refs for stateless functions components](https://github.com/facebook/react/issues/4213#issuecomment-115051991). (React currently warns if you try attaching a `ref` to a functional component, since there is no backing instance to reference.) This appraoch would not enable class components to forward refs, and so would not be sufficient, since wrapper components often require class lifecycles. It would also have the same child-depth limitations as the above option.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

appraoch -> approach

@AlmeroSteyn
Copy link

Hi @AlmeroSteyn. Are you talking about this section of the React docs? If so, feel free to submit a PR at any point if you have ideas to improve the docs. 😄

@bvaughn Yes that is it exactly! I will definitely allign that doc in the near future.

@elado
Copy link

elado commented Mar 17, 2018

I don't know if there are plans on making ref forwarding a part of React yet, but I just wrote a quick library that allows hoisting instance methods from inner refs. It uses context (old API), and some JS magic.

First draft here:
https://github.com/elado/react-ref-method-forwarder

@bvaughn
Copy link
Collaborator Author

bvaughn commented Mar 17, 2018

I don't know if there are plans on making ref forwarding a part of React

That's what this RFC was for 😄and the feature is part of the upcoming 16.3 (currently available as an alpha on NPM).

@bvaughn
Copy link
Collaborator Author

bvaughn commented Mar 17, 2018

I think the final comment period is done for this PR.

@bvaughn bvaughn merged commit 0bdc11f into reactjs:master Mar 17, 2018
@bvaughn bvaughn deleted the ref-forwarding branch March 17, 2018 04:59
@elado
Copy link

elado commented Mar 19, 2018

@bvaughn @gaearon there's one use case I'm still not sure how React.forwardRef solves:

class Field extends Component {
  focusInput() {
    this.input.focus()
  }

  render() {
    return (
      <div>
        <input ref={i => (this.input = i)} />
      </div>
    )
  }
}

class App extends Component {
  render() {
    return (
      <div>
        <Field ref={i => (this.field = i)} />
        <button onClick={() => this.field.focusInput()}>Focus</button>
      </div>
    )
  }
}

when Field is wrapped with some HOC from a different library:

class Field extends Component {
  focusInput() {
    this.input.focus()
  }

  render() {
    return (
      <div>
        <label>{this.props.label}</label>
        <input ref={i => (this.input = i)} />
      </div>
    )
  }
}

// one or more HOCs from my code/other libraries
const WrappedField = someHOC(Field)

class App extends Component {
  render() {
    return (
      <div>
        <WrappedField ref={i => (this.field = i)} />
        {/* this will fail */}
        <button onClick={() => this.field.focusInput()}>Focus</button>
      </div>
    )
  }
}

In App I want this.field to point to the Field instance, not the someHOC instance. I can't change someHOC to forward refs.

Or even with Redux connect - I want to "skip" HOCs and access methods on my wrapped component. Specifically focus, scrollTo and methods that change UI state that is not controlled by React (e.g. setting isFocused on an element is not the solution because this UI state is ephemeral and user may blur it).

@satya164
Copy link
Member

In App I want this.field to point to the Field instance, not the someHOC instance. I can't change someHOC to forward refs.

This is meant for library developers writing the HOC, not for consumers.

But you could write your own HOC which renames forwards the ref as some prop (say forwardedRef, and then in your component, use that when assigning the ref for input.

@bvaughn
Copy link
Collaborator Author

bvaughn commented Mar 19, 2018

@satya164 is right. forwardRef is meant more for use by the developer writing the HOC than one using it. Ideally, someHOC would use forwardRef to do this for you automatically.

Worst case, assuming someHOC passes through props to your inner component, you can just intercept the ref and convert it to a prop, and maybe pass it through using something like this?

// Extracts a forwared ref-prop and applies as a ref
function applyForwardedRefTo(Component) {
  return function ApplyForwardedRef({ forwardedRef, ...props }) {
    return <Component {...props} ref={forwardedRef} />;
  };
}

// Intercepts an incoming ref and forwards it as a prop
function forwardRefTo(Component) {
  function ForwardRefTo(props, ref) {
    return <Component {...props} forwardedRef={ref} />;
  }
  return React.forwardRef(ForwardRefTo);
}

const WrappedField = forwardRefTo(someHOC(applyForwardedRefTo(Field)));

Here's a CodeSandbox example:
https://codesandbox.io/s/94k43kpm9p

@elado
Copy link

elado commented Mar 19, 2018

@satya164

This is meant for library developers writing the HOC, not for consumers

This means that current non-maintained libraries will probably never get this feature

Instead of waiting for updates/submitting PRs, I think there should be a global, bulletproof solution.

That's why I came up with https://github.com/elado/react-ref-method-forwarder in the first place.

React.forwardRef actually helps here, and still needs 2 HOCs (like react-ref-method-forwarder):

import React, { Component } from 'react'
// just a 3rd party library that exports a HOC
import dimensions from 'react-dimensions'

class Field extends Component {
  focusInput() {
    this.input.focus()
  }

  render() {
    return (
      <div>
        <input ref={i => (this.input = i)} />
      </div>
    )
  }
}

const forwardRefOuter = Wrapped => {
  return React.forwardRef((props, ref) => <Wrapped {...props} innerRef={ref} />)
}

// this *WORKS*, but throws a warning:
// Warning: Stateless function components cannot be given refs. Attempts to access this ref will fail.
// const forwardRefInner = Wrapped => props => <Wrapped {...props} ref={props.innerRef} />


const forwardRefInner = Wrapped => {
  return class extends Component {
    render() {
      return <Wrapped {...this.props} ref={this.props.innerRef} />
    }
  }
}

export default forwardRefOuter(dimensions()(forwardRefInner(Field)))

/* or
@forwardRefOuter
@dimensions()
@forwardRefInner
class Field...
*/

// App.js - normal ref usage

class App extends Component {
  render() {
    return (
      <div>
        <Field ref={i => (this.field = i)} />
        <button onClick={() => this.field.focusInput()}>Focus</button>
      </div>
    )
  }
}

Edit: Snap 😄 @bvaughn beat me me to it ;)

@gaearon
Copy link
Member

gaearon commented Mar 19, 2018

This means that current non-maintained libraries will probably never get this feature

This goes for any RFC. We’ve always been improving the way React apps and libraries are written in the future rather than adding retroactive hacks that get around an abstraction. If something is unmaintained it will likely bump into breaking changes between majors in any case. I understand it’s not always convenient, but it’s a larger discussion and unrelated to this particular API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.