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?
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鈥檚 just functions so you can do something and then delegate to the other function.
Ah great. Yes, I misunderstood the meaning behind callback-refs
in the docs, but thanks for walking me through a solution. I appreciate it.
@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
?
Yea I think you're right. You need to.
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.
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);
}
}
})
);
Thanks! I fixed it up in my comment so that people don't get confused.
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?
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.
@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
.
@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? 馃檭
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
The TypeScript typings are maintained by the community over at DefinitelyTyped. If you're missing a property feel free to submit a patch.
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
.
@eemeli - Can you provide an example of how the ref is applied to the child DOM element in the above scenario?
@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>
}
}
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.
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.
Most helpful comment
I think you might have missed this paragraph:
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 overrideref
.However, indeed, it is possible to keep both refs with callbacks. You just need to do it manually.
Based on your example:
It鈥檚 just functions so you can do something and then delegate to the other function.