Closed gaearon closed 8 years ago
Some ideas to ease the transition:
Automatically reference the top node rendered on the component
class MyComponent extends Component {
componentDidMount() {
this._topDOMNode.scrollIntoView();
}
render() {
return <div />
}
}
Somehow do shorthand aliasing for the ref prop:
class MyComponent extends Component {
componentDidMount() {
this.node.scrollIntoView();
}
render() {
return <div ref={this.node} />
}
}
Provide a reference to the DOM node separately from the element reference:
class MyComponent extends Component {
componentDidMount() {
this.nodeRefs.myNode.scrollIntoView();
}
render() {
return <div ref="myNode" />
}
}
In my opinion it seems like one of those cases where the only reason people want shortcuts is because they’ve been exposed to shorter magic syntax. Shorthands might seem “nice” but they actually make less sense coming from a beginner’s perspective. It’s easier to learn how the system works once than remember that topDOMNode
is automatic but for everything else you need to use refs, or that this.node
is somehow going to turn into a magic ref but there is just one such ref. As for the string suggestion, we won’t go with it because string refs are problematic by themselves, and so we want to get away from them as well.
@gaearon hey, can you leave a short note or is there a link to read why ref callbacks are preferred to ref strings? Thanks
@gaearon Great idea! However, is this in addition to a warning within React itself? If the React team definitely wants to deprecate this, seems like it should definitely warn there, as well. Not everyone may use ESLint, and IMO, it's not ESLint's responsibility to notify users about feature deprecation.
@notaurologist There isn't a feature deprecation, just a potentially bad pattern, which is definitely ESLint's domain.
@timdorr I agree, but as @gaearon said: "We want to deprecate it eventually (not right now) because it blocks certain improvements in React in the future." I think this definitely warrants an additional warning in React.
Suppose that I want to create a behavior component that acts on the DOM of its provided child (like a fake mutation observer, for instance):
class Measure extends Component {
componentDidMount() {
const childNode = findDOMNode(this).children[0];
// Here I call whatever I want when the child is loaded
// @NOTE: There's no refs implied.
}
render() {
const { children } = this.props;
// Here, I'm agnostic to whatever the child might be, a pure function or a class
return children;
}
}
Now, as I'm agnostic to the type children passed like this:
<Measure>
<AnyChildWithoutRefsLikePureComponent/>
</Measure>
I could clone the AnyChildWithoutRefsLikePureComponent
, check it its a Component
or a pure function, and if it's a pure function, turns it into a Component
just to get a dynamic ref on it. But that would defeat the whole purpose of being agnostic to Component
vs pure function.
@gaearon small side note, in the after block - I believe the findDOMNode(childComponentStringRef)
example is supposed to read this.inputNode.focus();
rather than this.inputNode.scrollIntoView();
There's an example here of what @PEM-- described.
Basically, it's an element that pins its children scroll position to bottom as it grows.
const { render, findDOMNode } = ReactDOM
class PinToBottom extends React.Component {
/// ...
componentWillUpdate() {
const node = findDOMNode(this)
// ...
}
componentDidUpdate() {
if (this.pinToBottom) {
const node = findDOMNode(this)
node.scrollTop = node.scrollHeight
}
}
render() {
return React.Children.only(this.props.children)
}
}
And then it can be used by any content container, like that:
<PinToBottom>
<ul>
{lines.map((line, index) => (
<li key={index}>{line}</li>
))}
</ul>
</PinToBottom>
I don't know how it could be made simpler by using callback refs or something else.
How do you suggest one deals with higher order functions over non-dom components, such as
var Wrapper = ComposedElement => class extends React.Component {
componentDidMount() {
// if ComposedElement is not a DOM component
// this.domNode <> ReactDOM.findDOMNode(this)
}
render() {
return <ComposedElement ref={r=>{this.domNode = r}}/>
}
};
@alaindresse why would that work any differently using this.domNode
? the wrapped component would still work the same with the ref as it would with findDOMNode.
I'm also wondering how is HOCs like this should be re-written then?
https://github.com/okonet/react-container-dimensions/blob/master/src/index.js
Your third example's "After" doesn't match with "Before".
this.inputNode.scrollIntoView();
Should be
this.inputNode.focus();
Actually, it seems to me that all examples drill down to the same behavior. If we could setup a callback ref on a children passed as prop, the findDOMNode
could then be removed.
A function like React.Children.addCallbackRef
could do it.
Sounds legitimate. A parent can call its children 😉
@gaearon, what do you think of this?
@timdorr I agree, but as @gaearon said: "We want to deprecate it eventually (not right now) because it blocks certain improvements in React in the future." I think this definitely warrants an additional warning in React.
Deprecating is adding a warning. So we plan to add a warning but in the future. I thought that maybe ESLint rule might be a better first idea before we deprecate it.
@ljharb in the HOC, you don't know if ComposedElement is a DOM or a react class. The reference is then either a DOM node, or an instance of a react class. In the latter case, you need findDOMNode to get the actual dom node...
One idea would be to have two arguments in the ref callback
ref={(r,n)=>{this.component = r; this.node = n}
if the component is a dom node, r===n. If the component is a react class, n is the topDOMNode @timdorr referred to earlier.
Suppose that I want to create a behavior component that acts on the DOM of its provided child
Yes, HOCs like this would have to wrap their content in an extra <div>
. This sounds bad until you see that the whole concept is flawed.
People often request that React adds support for returning multiple nodes from render (“fragments”). Imagine we implement this. Now any component can either return zero, one, or many nodes.
Somebody changes one of the “measured” components to return two nodes in some state. What should findDOMNode
return? First node? An array?
Whether a component returns many nodes is its implementation detail. It should be possible to change it without breaking any code in other components, but this would not work with our HOC. So the pattern itself is problematic.
There are two solutions:
<div>
. This is the easiest way to preserve encapsulation.refToMeasure
so wrapped component can use <div ref={this.props.refToMessure}>
. This is identical to how my last example works. Components explicitly exposed nodes they want to.Reading nodes of child components is like wanting to access their state. This is not a pattern we should support or allow (even if it is technically possible). If it was unsupported from day one, I don’t think it would be much of a controversy. However it is less obvious that the pattern is problematic because it’s been possible for a while.
If we could setup a callback ref on a children passed as prop, the findDOMNode could then be removed.
You can as long as children are DOM elements. You can check for that with this.props.children && typeof this.props.children.type === 'string'
. In this case, to attach a callback ref, you can use cloneElement
with a ref
callback that calls the original ref
function if it exists, and then does whatever you needed.
For reasons above you cannot do this on custom components.
Indeed, I agree. It's like having a form that parses its input fields instead of giving the fields the capabilities to inform the form itself. It's parsing against eventing. And that's against the purpose of React 👍
DOM based operation should ensure that a parsable DOM is present.
@PEM @fhelwanger
its possible to clone children with ref callback added to it, so it can be exposed to a wrapper component
@Andarist: Thanks but it only works if your children are immediate DOM elements 😉
@Andarist @PEM-- Yes! The nice (or bad :smile:) thing about findDOMNode
is that it can be another React.Component
, something like:
<PinToBottom>
<List>
{lines.map((line, index) => (
<ListItem key={index}>{line}</ListItem>
))}
</List>
</PinToBottom>
And it'll find its DOM node anyway.
findDOMNode
.One can argue that by using findDOMNode
you're breaking the component's encapsulation, but besides that, I think that for this particular use case findDOMNode
is more straightforward than cloneElement
+ callback refs. But maybe it's just me :wink:
Just read @gaearon comments, and I agree 1000% with:
Whether a component returns many nodes is its implementation detail. It should be possible to change it without breaking any code in other components, but this would not work with our HOC. So the pattern itself is problematic.
Now that I understand the problem better and how this make returning multiple nodes from render very difficult, I rewrote the example to wrap every children of PinToBottom
inside a div
.
It's much cleaner and doesn't break encapsulation!
To come back to the ESLint rule
Deprecating is adding a warning. So we plan to add a warning but in the future. I thought that maybe ESLint rule might be a better first idea before we deprecate it.
I'm agree, also adding a rule like this is pretty easy (we've already done something similar for isMounted()
).
As an additional example for those who need access to a component DOM node from its parent but also from the component itself, here is the modified code from @gaearon example:
class Field extends Component {
render() {
return (
<input type='text' ref={node => this.props.inputRef(node) && (this.node = node)} />
)
}
}
It seems somewhat trivial but it took me a bit of time to figure it out so it may help anybody with the same use case.
Another problem I can come up with, is when it comes to animating properties like scrollTop
. Animating scrollTop
with any animation library requires the actual DOM node. For example, the only way to add animation to @fhelwanger's pin to bottom codepen example would be to manually hardcode the animation logic, instead of being able to reuse Velocity
or jQuery
, etc
@perrin4869 I described this use case in the very first post in this thread. You can get the DOM node without findDOMNode()
, just use a ref.
@perrin4869 Check this: https://github.com/yannickcr/eslint-plugin-react/issues/678#issuecomment-232332949
You can use the ref available inside PinToBottom
to get the DOM node you need. Then you can pass it to jQuery, Velocity, etc.
Hm... I'm confused then. In the snippet below, is there any difference between this.node
and this.domNode
, and could it affect how animation libraries work?
class Foo extends Component {
componentDidMount() {
this.domNode = findDOMNode(this.node);
}
render() {
return <div ref={c => { this.node = c; }} />;
}
}
@perrin4869 You don't need this.domNode = findDOMNode
for it to work. The ref
prop on your div
already gives you the DOM element. Thus, this.node
is already your DOM element.
You can do something like the following:
class Foo extends Component {
componentDidMount() {
$(this.node).slideDown();
}
render() {
return <div ref={c => this.node = c } />;
}
}
@perrin4869 the use case where there is a difference, is illustrated by @gaearon in the last example of his first post, is when you reference a react component - and not already a DOM element like the div
in your example - and need to access the actual DOM node of this component (eg to use the focus()
method of an input tag).
In that case, the children component needs to expose a prop to forward the DOM node to its parent, as shown in the example, where previously the parent would just use a ref on the children component and call findDOMNode()
@pandaiolo but you probably shouldn't be accessing the DOM node from another component.
Isn't the TextField
a good example? I also have a video component enriching the video
DOM element, which I need to access props and methods (play()
, pause()
, paused
, currentTime
, etc). Is there a reason I would not let the parent access the video
DOM node?
I think the best solution would be to let the video component have a isPlaying
prop, which changes trigger the required methods on the video component.
Something like this:
class Video extends React.Component {
getDefaultProps() {
return { isPlaying: false }
}
componentDidUpdate(prevProps) {
if(this.props.isPlaying !== prevProps.isPlaying) {
this.props.isPlaying ? this.video.play() : this.video.pause()
}
}
render() {
return <video ref={(el) => this.video = el} src="..."></video>
}
}
The component is simple and just exposes the video events as a prop. I could explicitly rewire all the native methods and props as well, but i'm not sure the added complexity is worth the linter code in that case. In the end that would just mean putting back the video
element in the parent component.
Thanks for pointing this anyway, I fully adhere to the "React way" so I understand that based on the simple example, it makes sense to ditch the external ref!
Are you using React without Redux or flux or anything? Because then I can understand. But when you do use one of those, you really want to control the video using the state of the application.
I do in the parent component which concern is about video control. Putting the video
element back there would add rather than remove complexity / readability - but yeah it's my call :)
Isn't the TextField a good example? I also have a video component enriching the video DOM element, which I need to access props and methods (play(), pause(), paused, currentTime, etc). Is there a reason I would not let the parent access the video DOM node?
In this case you can put them on the TextField
instance. Then the parent will have a ref to that instance, and call methods on it. And TextField
will use its own ref to DOM element.
play() {
this.node.play()
}
etc
It’s more typing but it’s also more explicit and doesn’t leak implementation details.
This is the Law of Demeter
deprecating findDOMNode is a an alarmingly upseting idea... Unless you are adding back getDOMNode(), it's going to make writing a large amount of react code just so so terrible. "Add and extra DOM node to use callback refs" is really really untenable for a ton of use cases; along with, effectively making interop with css frameworks not written with react almost impossible.
not to mention it effectively just ignores the fact that multiple layers of HOCs are the norm now thanks to mixin "deprecation" :P
This sounds like an idea from folks who nicely own every single component they use. for those using libraries... oof
If you have specific examples of problematic patterns please post them in the thread. This is exactly what I did in the post, and it would really help us weigh on this decision (and maybe think of better alternatives). It would help us a lot if you could do the same thing. You might think your concerns are obvious and we are intentionally ignoring them. In reality it's not at all obvious what you are concerned about without code samples. Thanks!
I also believed I explained above in this thread why findDOMNode
API is fundamentally incompatible with some features long requested by the community (such as fragments). We are not deprecating it just because we enjoy deprecating things. :wink:
The case where this comes up most acutely is having a positioner component for e.g. tooltips and overlays.
Consider:
<Button ref={c => { this.button = c; }} />
<Position target={this.button}>
<Tooltip>Some tooltip</Tooltip>
</Position>
For <Position>
to work as a generic component that can e.g. calculate the size of its child, then position it appropriately relative to its target, it needs to be able to get the child's DOM node.
To be compatible with e.g. Bootstrap styling, it's not generally possible for a component like <Position>
to render an extra <div>
.
<Position>
just adding a ref to its child is insufficient, since its child may well be some composite component (e.g. <Tooltip>
above) rather than a DOM component.
The category in general is utility components that need to interact with the DOM nodes belonging to their children, for functionality such as positioning, animation, &c.
If you control all of your own styling, then perhaps the extra <div>
is acceptable, though not ideal. If you don't control your CSS, then adding an extra <div>
can be a non-option.
Sorry if I sound annoyed, these sort of drastic changes can feel like they are coming out of the blue. A proper RFC process for this stuff would really be helpful here.
to add to @taions point, so also for focus management, which is required for accessible components, right now react just offers nothing useful for that problem which requires dropping down to DOM nodes, and not wrapping ones in the HOC case. imperative methods sort of work but are often and frustratingly obscured by HOCs, and just not possible for function components.
The extra DOM nodes are often presented as a "just do that" sort of simple solution but I feel like that ignores that layout and styling in the DOM is extremely intolerant to adding in bonus nodes. not to mention the lack of semantic value. To be honest I think most often the reason want folks want fragments (maybe one of the most requested API changes?) in the first place is exactly to avoid these bonus nodes.
I appreciate the desire to really abstract away the DOM as a render target, but these escape hatches are important not the least of which because the DOM is not particularly a good target for this sort of thing and wildly quirky (I encourage folks to look at the react event code to see just what is needed for consistency on that front).
Saying you can only drop to the host objects via function refs feels like a line drawn against an arbitrary and unhelpful line. Escape hatches should be used prudently and judiciously but they should be easily and constitently accessible, otherwise they aren't useful in any case. This is the same problem with stateless function components not being ref-able. there is no real reason to think someone would not need the escape hatch for those components over classy ones.
it seems like if findDOMNode is blocking other features then perhaps change they way it works, instead of removing it? Personally I'd be fine if it sometimes returned an array of nodes vs a single one, jQuery has done this with a ton of success forever
If you have specific examples of problematic patterns please post them in the thread.
@gaearon I ran into a problematic pattern today where I think I need findDOMNode
. This is a bit of an unusual situation and if you can see an alternate solution, I would love to know.
I am currently working on setting up Happo to do visual regression testing in CI. The basic idea is we have some code that:
In order to get any component's coordinates, we need a reference to the DOM node. Because you can't use ref
on SFCs, we have a basic wrapper component that we wrap when rendering these examples:
class Wrapper extends React.Component {
render() {
return React.Children.only(this.props.children);
}
}
Then, inside our example, we have some code like:
const container = document.createElement('div');
document.body.appendChild(container);
let elemRef;
const elem = (
<Wrapper ref={(ref) => { elemRef = ref; }}>
{example}
</Wrapper>
);
ReactDOM.render(elem, container);
setTimeout(() => done(ReactDOM.findDOMNode(elemRef)), 0);
From this DOM node reference, we find the coordinates via getBoundingClientRect()
.
Perhaps I've overlooked something, but is there currently a way to achieve this without ReactDOM.findDOMNode()
? We thought that perhaps we could wrap it in an inline-block <div>
with a ref (which then gives us a DOM node), but that changes the styling characteristics of some components (e.g. display: block
elements don't stretch out).
Is there an issue tracking this on facebook/react?
It's a bit weird to discuss it here. The lint rule is fine, and I think we all agree that many or even most uses of findDOMNode
are better handled with a ref on a DOM component.
The eslint-plugin-react repo is not the right place to discuss whether findDOMNode
should exist as a React DOM feature.
Please feel free to file a discussion on React repo, sure!
This is the same problem with stateless function components not being ref-able.
What would you expect to get as a reference to a functional component? Since it doesn’t have an instance, it’s not clear what you are suggesting.
@lencioni
Wouldn’t this work for your use case?
const container = document.createElement('div');
document.body.appendChild(container);
ReactDOM.render(example, container);
setTimeout(() => done(container.firstChild), 0);
There are almost no situations where you’d want to use
findDOMNode()
over callback refs. We want to deprecate it eventually (not right now) because it blocks certain improvements in React in the future.For now, we think establishing a lint rule against it would be a good start. Here’s a few examples of refactoring
findDOMNode()
to better patterns.findDOMNode(this)
Before:
After:
findDOMNode(stringDOMRef)
Before:
After:
findDOMNode(childComponentStringRef)
Before:
After:
Other cases?
There might be situations where it’s hard to get rid of
findDOMNode()
. This might indicate a problem in the abstraction you chose, but we’d like to hear about them and try to suggest alternative patterns.