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

Maintaining ref prop through React.cloneElement() #8873

Closed
pvienneau opened this issue Jan 26, 2017 · 18 comments
Closed

Maintaining ref prop through React.cloneElement() #8873

pvienneau opened this issue Jan 26, 2017 · 18 comments

Comments

@pvienneau
Copy link

React's 0.13 RC suggests that ref prop on components pushed through React.cloneElement() will allow for two parents to maintain ref props to the same child.

I've tried to replicate this behaviour in a CodePen, but I am not able to maintain two ref references to the same child (ie the ancestor component's ref gets nulled).

Here is the jist of the code, working code found here:

class ChildComponent extends React.Component{
  constructor(props){
    super(props);   
  
    this.onClick = this.onClick.bind(this);
    this.extendsChildren = this.extendChildren(this);
  }
  
  onClick(e) {
    e.preventDefault();
    
    try{
      alert(this._input.value);
    }catch(e){
      alert('ref broken :(');
    }
  }
  
  extendChildren(){
    return React.Children.map(this.props.children, child => {
      return React.cloneElement(
        child,
        {
          ref: ref => this._input = ref
        }
      );
    });
  }
  
  render() {
    return(
      <div>
      <button onClick={this.onClick}>
        ChildComponent ref check
      </button>
      {this.extendChildren()}
    </div>
    );
  }
}


class AncestorComponent extends React.Component{
  constructor(props){
    super(props);
    
    this.onClick = this.onClick.bind(this);
  }
  
  onClick(e) {
    e.preventDefault();
    
    try{
      alert(this._input.value);
    }catch(e){
      alert('ref broken :(');
    }
    
  }
  
  render() {
    return (
    <div>
        <p>
          The expected behaviour is that I should be able to click on both Application and ChildComponent check buttons and have a reference to the input (poping an alert with the input's value).
        </p>
      <button onClick={this.onClick}>
        Ancestor ref check
      </button>
      <ChildComponent>
        <input ref={ref => this._input = ref} defaultValue="Hello World"/>
      </ChildComponent>
    </div>
    );
  }
}

Has this behaviour been dropped/never implemented since the above RC? Or am I doing something wrong?

@gaearon
Copy link
Collaborator

gaearon commented Jan 26, 2017

I've tried to replicate this behaviour in a CodePen, but I am not able to maintain two ref references to the same child (ie the ancestor component's ref gets nulled).

I think you might have missed this paragraph:

Note: React.cloneElement(child, { ref: 'newRef' }) DOES override the ref so it is still not possible for two parents to have a ref to the same child, unless you use callback-refs.

It's unfortunately not very clear, but the point was that if you override ref it still gets overwritten. The change in RC was related to the case where you just clone but don't override ref.

However, indeed, it is possible to keep both refs with callbacks. You just need to do it manually.
Based on your example:

return React.Children.map(this.props.children, child =>
  React.cloneElement(child, {
    ref: (node) => {
      // Keep your own reference
      this._input = node;
      // Call the original ref, if any
      const {ref} = child;
      if (typeof ref === 'function') {
        ref(node);
      }
    }
  })
);

It’s just functions so you can do something and then delegate to the other function.

@gaearon gaearon closed this as completed Jan 26, 2017
@pvienneau
Copy link
Author

Ah great. Yes, I misunderstood the meaning behind callback-refs in the docs, but thanks for walking me through a solution. I appreciate it.

@arnemahl
Copy link

@gaearon Don't you need to bind the ref(node) function, or turn it into an arrow function, to make it access the correct this?

@gaearon
Copy link
Collaborator

gaearon commented Mar 17, 2017

Yea I think you're right. You need to.

@gaearon
Copy link
Collaborator

gaearon commented Mar 17, 2017

Or maybe not. :P

I don't remember whether React takes care to call it with the right instance or not. You can try and let me know.

@arnemahl
Copy link

Tested it to make sure :) this was undefined.

Apart from that it works perfectly. Below is the fixed snippet (also added a missing }).

return React.Children.map(this.props.children, child =>
  React.cloneElement(child, {
    ref: (node) => {
      // Keep your own reference
      this._input = node;
      // Call the original ref, if any
      const {ref} = child;
      if (typeof ref === 'function') {
        ref(node);
      }
    }
  })
);

@gaearon
Copy link
Collaborator

gaearon commented Mar 20, 2017

Thanks! I fixed it up in my comment so that people don't get confused.

@d07RiV
Copy link

d07RiV commented Dec 20, 2017

How does this work - why is ref a property of child itself, and not of its props? Is that implementation detail documented anywhere? Can we rely on it?

@gaearon
Copy link
Collaborator

gaearon commented Dec 20, 2017

How does this work - why is ref a property of child itself, and not of its props?

Because it doesn't behave like a prop. A component can't read its own ref (by design). Just like key, it's a React-specific property and doesn't become a part of the props.

Can we rely on it?

Yes.

goldylucks added a commit to goldylucks/react-input-trigger that referenced this issue Apr 3, 2018
current behavior overrides the child's ref. [Read this](facebook/react#8873 (comment)) for more info.
@bananatranada
Copy link

@gaearon Please forgive me if I'm wrong (I have a hard time understanding the code), but that keeps references of the original children (not rendered) and the cloned version, right? The old children is closed over, yes? child.ref() makes a call to the non rendered, original children's ref.

@craigkovatch
Copy link

@gaearon has this advice changed in 16.3+?

Also, latest React typings do not expose a 'ref' property on a ReactElement<P>, despite the property being readable at runtime. Am I doing something bad? 🙃

@maxwang7
Copy link

maxwang7 commented Dec 1, 2018

bumping this thread, I'm following the advice of this thread by calling the original ref callback in the overriding ref callback but also not seeing the ref property in the typings

@eps1lon
Copy link
Collaborator

eps1lon commented Mar 19, 2019

The TypeScript typings are maintained by the community over at DefinitelyTyped. If you're missing a property feel free to submit a patch.

@eemeli
Copy link

eemeli commented May 6, 2019

Here's a small update to @gaearon's manual method that also handles createRef() refs:

return React.Children.map(this.props.children, child =>
  React.cloneElement(child, {
    ref: (node) => {
      // Keep your own reference
      this._input = node;
      // Call the original ref, if any
      const {ref} = child;
      if (typeof ref === 'function') {
        ref(node);
      } else if (ref !== null) {
        ref.current = node;
      }
    }
  })
);

That's based on what the React internals appear to do: just set the value of ref.current.

@discrete-projects
Copy link

discrete-projects commented Jul 18, 2019

@eemeli - Can you provide an example of how the ref is applied to the child DOM element in the above scenario?

@eemeli
Copy link

eemeli commented Jul 18, 2019

@discrete-projects Something like this? When Thing props change, both doSomething() and doSomethingElse() should be called with the #dom-thing element. Fair warning, I've not tested this code.

class Wrapper extends React.Component {
  _input = null

  componentDidUpdate() {
    doSomething(this._input)
  }

  render() {
    return React.Children.map(this.props.children, child =>
      React.cloneElement(child, {
        ref: node => {
          this._input = node
          const { ref } = child
          if (typeof ref === 'function') ref(node)
          else if (ref) ref.current = node
        }
      })
    )
  }
}

export class Thing extends React.Component {
  _ref = React.createRef()

  componentDidUpdate() {
    doSomethingElse(this._ref.current)
  }

  render() {
    return <Wrapper {...this.props}>
      <div id="dom-thing" ref={this._ref} />
    </Wrapper>
  }
}

@ghost
Copy link

ghost commented Jan 26, 2020

Interesting how we can do such a solution that you suggested @gaearon but also works for both: ref functions and useRef 🙂 Child can pass ref as a function or pass the useRef object and it works as expected with the parent being use useRef internally.

@d07RiV
Copy link

d07RiV commented Nov 6, 2020

This creates big issues when trying to only use createRef/useRef, since in this case you're forced to use ref functions, and the rest of the code must also now expect ref's to be functions sometimes. I.e. all forwardRef components can't just set current value, they need to check if the ref is a function first.

There needs to be a createRef compliant way to achieve this, without using functions.

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

No branches or pull requests

10 participants