It seems the ref
callback isn't behaving as I expected it to, based on the docs.
The docs say that ref
is called when the component is mounted (and all the other times it's called, it's called with null
as a parameter). However, when I put a console.log
in componentDidMount
, and one in the ref
callback, the one in the ref
callback is called on every render()
, whereas the componentDidMount
is only called once. The component i'm reffing isn't even changing props.
Am I misunderstanding something? I'm using React 0.14.
var Value = React.createClass({
componentDidMount() {
console.log("Mounted");
},
render() {
return <div>Dummy</div>;
}
});
var Hello = React.createClass({
getInitialState() {
return { value: 0 };
},
componentDidMount () {
setInterval(() => { this.setState({value: this.state.value + 1})}, 1000);
},
render: function() {
return (
<div>
<span>{this.state.value}</span>
<Value ref={(e) => { if (e) { console.log("ref", e); }}} />
</div>
);
}
});
ReactDOM.render(<Hello/>, document.getElementById('container'));
This is intended (discussed elsewhere) but rather unintuitive behavior. Every time you render:
<Value ref={(e) => { if (e) { console.log("ref", e); }}} />
You are generating a new function and supplying it as the ref-callback. React has no way of knowing that it's (for all intents and purposes) identical to the previous one so React treats the new ref callback as different from the previous one and initializes it with the current reference.
PS. Blame JavaScript :P
Oh, I see. So that's simple to work around, then, thanks!
Maybe the example in the docs should be updated, because it's now also using an inline callback to focus an element, which would have some weird effect on every render. I'm closing the issue though, since my issue is resolved.
I am confused, what's an easy work-around for this issue? Even if I do something like that ref={this.func.bind(this)}
I get the same problem. Even worse, if I have a setState
in func
that I get an endless loop.
Since the functions have to be bound to access this
the only solution I see to get the actual expected behavior is something like:
func2 = function() {
console.log("got ref 2", this);
}.bind(this)
and then doing ref={this.func2}
This kind of mess makes ref
callback rather annoying/unreliable to use.
I've made a jsbin of the issue here: https://jsbin.com/poboqo/1/edit?html,js,output
@vitalybe I'm guessing this is why many React examples that use ES6 classes use trailing/leading underscores for method names, and bind them in the constructor to this
(without the underscore). I'm also guessing this isn't a problem with React.createClass
, which strangely enough would make createClass
cleaner to use than ES6, but more knowledgeable people can tell me I'm wrong.
I am confused, what's an easy work-around for this issue?
With ES6 class properties and arrow functions, you can do this:
class MyComponent extends React.Component {
callback = () => {
console.log(this)
}
render () {
return <div ref={this.callback} />
}
}
Here the callback
function is added as a class property, which makes sure that the this
binding is always the current instance. That's because doing callback = () =>
is roughly the same as:
constructor () {
this.callback = () => {
console.log(this)
}
}
@vitalybe What is your use case for refs? If you just set an instance field it should make absolutely no difference whether it is reattached on every render or not.
@goto-bus-stop Thanks for the snippet, that indeed looks better.
@gaearon The problem was that I was causing, by proxy, a setState()
to be called by the ref callback function, causing a never-ending loop. If I was merely saving the element, it's true that it wouldn't matter much.
We've mentioned several solutions to this issue on the thread, I think that the main point, however is that its current behaviour is rather unexpected.
We should probably throw early if you setState from a ref callback. I don't think this is meant to be a supported pattern.
@syranide Whoa. :beers: to you! So I should probably refactor all my code that looks like:
<div ref={node => node && this.node = node}>
@ffxsam On the contrary, we give you null
so that you don鈥檛 accidentally keep references to dead DOM nodes and components, potentially preventing them from getting garbage collected.
Oh wait, I misunderstood your comment. Yes, you need to refactor from that pattern to a simple:
<div ref={node => this.node = node}>
<div ref={node => this.node = node}>
This doesn't work well for me though. When I use this pattern, as the OP said, the ref callback fires off every time the component renders. I should mention, that div is the top-level BTW:
render() {
return <div ref={node => this.node = node}>
I don't know if that's the reason I was getting repeated ref callback invocations?
the ref callback fires off every time the component render
Why is this a problem for you? This is the expected behavior.
It is expected because https://github.com/facebook/react/issues/6249#issuecomment-195412139.
In case you鈥檙e worried about bottlenecks, it is extremely unlikely that setting a field is a performance bottleneck in your app. In the extreme case when it is, you can hoist it to be a bound class method (just like you do with event handlers), and you鈥檒l avoid extra calls.
I understand why it's getting called upon re-renders. Sort of.. I'll get back to that in a second.
What you suggest (<div ref={node => this.node = node}>
) is of course, perfectly fine. Except in my scenario where I'm relying upon the ref callback to trigger another callback to push information about the DOM node into an array. In this case, I would get several duplicates of the node because the ref callback is fired on re-render, _not_ just on DOM node mount. The obvious solution for my case is to use <div ref={this.somethingMounted}>
.
What I don't understand is why the ref callback is called upon re-render. I get that it's a new function every time, but shouldn't the determining factor be whether the DOM node has already mounted or not? In other words, let's say I have this:
render() {
return <div>
<img ref={() => blahblah('abc')} src="..." />
</div>
}
Rather than the internal React logic saying (upon re-render), "Hey, this is a new function, I should call it" - shouldn't it instead say "I've already invoked a ref callback for img, so I won't do it again"?
Except in my scenario where I'm relying upon the ref callback to trigger another callback to push information about the DOM node into an array
Could you just do it in componentDidMount
and componentDidUpdate
instead?
What I don't understand is why the ref callback is called upon re-render.
shouldn't the determining factor be whether the DOM node has already mounted or not
Imagine this case:
<img ref={this.props.isAvatar ? this.handleAvatarRef : this.handleNormalRef} src="..." />
It鈥檚 a bit contrived but it illustrates the API makes it possible to pass different callbacks every time.
If it is possible then somebody will do it. (Maybe, with a layer of indirection, but still.)
If we didn鈥檛 clean up the old ref, it would be a terrible footgun the user actually intends to pass a different ref because we wouldn't respect it. So they would keep the old reference but not set a new one. This would make the initial mount behave inconsistently from update, which is against how other React APIs (e.g. props) work.
I hope this makes sense.
<img ref={this.props.isAvatar ? this.handleAvatarRef : this.handleNormalRef} src="..." />
Maybe I'm thinking about this wrong, but I would still expect this to fire only once. So if the component mounts and props.isAvatar
is false, it would call this.handleNormalRef
and that's it. I always pictured ref
callbacks as a sort of componentDidMount
but for individual DOM nodes, in that they only fire once the node in question is rendered for the first time. But again, maybe I'm thinking about it wrong the way.
Could you just do it in componentDidMount and componentDidUpdate instead?
I thought about that, but I need the actual DOM node (so I can run .getBoundingClientRect()
on it), which would require me to do this:
componentDidMount() {
const node = this.refs.rootNode;
this.props.onMount({ rect: node.getBoundingClientRect() });
}
render() {
return <div ref="rootNode"> ... </div>
}
To provide some context: I'm building a component that renders file icons and folders (each as a React component), and each <Folder>
component reports to its parent upon mounting, so it knows the rect coordinates for each folder icon. That's used when dragging files to know whether the user is hovering over a folder or not (and which one).
So if the component mounts and props.isAvatar is false, it would call this.handleNormalRef and that's it.
That's not how React works generally. There is no other case where "initial props" are in any way special to "updated props". Usually React can update any prop but this would be a case where the first value "gets stuck".
I thought about that, but I need the actual DOM node (so I can run .getBoundingClientRect() on it), which would require me to do this:
Why use string refs there?
componentDidMount() {
this.props.onMount({ rect: this.rootNode.getBoundingClientRect() });
}
render() {
return <div ref={node => this.rootNode = node}> ... </div>
}
Oh. Ok, yeah, that makes a lot of sense. 馃榿 So componentDidMount
is fired off after all the ref
callbacks have been executed? That would totally work.
(dumb code removed)
Thanks for clarifying all this, Dan!
So componentDidMount is fired off after all the ref callbacks have been executed?
Yes.
Except when the component is unmounting, where ref gets called again with a null value.
No, this is not necessary.
How could you get componentDidMount
when a component is unmounting?
Er, no - I know this. I know React! 馃槅 Sorry, I replied in haste.
I am seeing something strange about this. This is what I am understanding:
If a component updates it will simply call ref
with a reference to the DOM element. The only time ref
is called with null
is when it unmounts. If I am following correctly, then why is this occurring:
https://jsfiddle.net/dflores009/rwpfrge0/
Every time the Controlled input
get's updated with a new value, it get's called with null
and instantly calls it again with a reference to the DOM element. Shouldn't ref
always be called with a reference to the DOM since it did not unmount. You can see that componentWillUnmount
was never called as you type and it updates the state.
Straight from the docs:
React will call the
ref
callback with the DOM element when the component mounts, and call it withnull
when it unmounts.
There are only two references to null
in that page of documentation and the other is a code example, so @Intregrisist is absolutely correct. In his example, null
is passed on every key press of the text box, even though the component is never unmounted.
Either the documentation needs to be changed to clarify this is expected behavior or something needs to be fixed here. I hope we can all agree the latter is the case, because this would be very weird expected behavior!
Oh wait, I misunderstood your comment. Yes, you need to refactor from that pattern to a simple:
<div ref={node => this.node = node}>
Or just
<div ref={node => { this.node = node}}>
to avoid add an unnecessary return
to your function 馃槅
Also thanks for clearing that up, been looking in the documentation about that, also thought ref
acts like componentDidMount
.
The docs were updated, just not published to website yet. See last section.
@jedmao @Intregrisist
Have you had a chance to read this thread? Yes, the docs were incomplete, but I believe it has been explained why this is happening.
The explanation is both in https://github.com/facebook/react/issues/6249#issuecomment-195412139 and our last conversation with @ffxsam.
I hope this helps.
You can supply the "bound" callback method instead so that the javascript does not inefficiently recreate the callback method and call it on every render.
Most helpful comment
This is intended (discussed elsewhere) but rather unintuitive behavior. Every time you render:
You are generating a new function and supplying it as the ref-callback. React has no way of knowing that it's (for all intents and purposes) identical to the previous one so React treats the new ref callback as different from the previous one and initializes it with the current reference.
PS. Blame JavaScript :P