-
Notifications
You must be signed in to change notification settings - Fork 46.9k
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
WIP RFC: Improvements to the "ref" system #11401
Comments
Unfortunately, your solution isn't resolving problem of receiving component instance in parent component. For example, you have a component from other library/developer, and it hasn't any props for refs. You will need to do a PR, and it will be merged in a week, month, or never. So, it will break many of existing apps using refs without any other way to achieve this functionality. |
I mentioned this a bunch on the eslint thread about deprecating findDOMNode with @taion but using props to pass refs for DOM node access is untenable for libraries that may not own either their parents or children. This approach only works if everyone agrees on the same prop names. Imagine if every library specified their own name for css classes, nothing would interop well because you'd need to handle The root problem with composite refs seems to be it encourages using the instance imperatively, we could address that by having the same behavior as host refs, and always return the underlying host node from the ref, which is probably the 90% of the use case for refs anyway |
There are alternative approaches to getting the root DOM node of a class component (for where you need it in special cases) via a wrapper component: class RootNodeWrapper extends React.Component {
componentDidMount() {
this.props.rootRef(ReactDOM.findDOMNode(this))
}
componentWillUnmount() {
this.props.rootRef(null)
}
render() {
return <this.props.children />
}
}
// usage
<RootNodeWrapper rootRef={...}><SomeChildComponent /></RootNodeWrapper> There are other ways to do this without using |
Strongly agreed with #11401 (comment). The CSS class name example is on point. These alternatives only work if there's some promulgated canonical name, like I do agree that always returning the host ref would be clever and neat, though. Still, the backdoor into getting the instance might be nice in a few cases. Not sure if cutting that off entirely is really best. |
I guess the So it'd be neat on web for Is that useful/meaningful in e.g. a React Native context though? |
@taion Thinking about this, I'm not sure the naming is as big a problem. Looking at different codebases, the contract for passing though ref via explicit props seems more declarative in my opinion. Rather than a generic As it's a composite component, you are free to name the prop as you want though, so you could use a generic name, even @jquense I feel that having that logic for composite components will become even more confusing when developing in React Native. The API for refs should remain consistent across all renderers in my opinion. |
@trueadm That works if you have a single level of nesting. If you have multiple levels of nesting, it sort of breaks down. If I want my |
@taion By using a generic name like |
I think in that case, you would just throw when attempting to attach <TooltipTrigger tooltip="do a foo">
<Button>Foo</Button>
</TooltipTrigger> <TooltipTrigger tooltip="do a foo">
<a>Foo</a>
</TooltipTrigger> Suppose I'd like for there to exist a convention where this "just works", where someone implementing Otherwise interop across component libraries becomes a hairy mess. |
@taion In the case you have above, I'd wrap the child component in a I really expect fragments to become the most common return type for component renders in the future and throwing an error on them isn't ideal at all for a core API. |
It's not generally acceptable to add extra wrapping markup for this sort of utility component – and these components really do come up quite a lot. As a generic utility component, we can't e.g. assume that the user's component can just be wrapped with something that's And while it may indeed be the case that most components will return fragments, it's also the case that most components don't need parents to inject e.g. event handlers into them to add e.g. "tooltip on hover" behavior. But components that care about this aren't going to return fragments anyway. That While react-call-return could be interesting as a longer-term solution, its semantics are quite different, and it can't e.g. be shimmed. From the perspective of something like that |
@taion In the case you expect a single component, then there's nothing stopping If you want to avoid class TooltipTrigger extends React.Component {
componentDidMount() {
this.props.ref && this.props.ref(this._markerNode.nextSibling);
}
componentDidUpdate() {
this.props.ref && this.props.ref(this._markerNode.nextSibling);
}
render() {
return [
<span ref={_ref => this._markerNode = _ref} stlye={{display: "none"}} />,
this.props.children,
];
}
} |
Your example per #11401 (comment) breaks down conceptually in the presence of fragment returns as well. And, I mean, that's fine – for my purposes here, I don't need a generic way to get a handle to or a DOM node for an arbitrary React element that may well be a fragment. I see this pattern on the Facebook page, I think. It looks a little bit like: <Tooltip tooltip="This is a tooltip">
<Something />
</Tooltip> It renders a tooltip in a portal (maybe not a React portal?) and positions it so that the tooltip shows up next to the This is a really common pattern generically. Almost every UI library exposes something like this, and often they don't make any assumptions beyond "the tooltip shows up on some element that can take mouse events" – certainly no mandatory extra markup. So in this case what I do want is something that implicitly asserts that |
Actually, is In which case, great, this is my preferred solution. Though I'm not sure how you'd handle that deprecation in such a way that doesn't warn for "legitimate" uses along those lines. |
As a motivating example, consider a component like: function Button({ className, ...props }) {
return (
<button
{...props}
className={classNames(className, 'my-btn')}
/>
);
} This is a pretty natural way to write a component. If |
@taion Maybe we should deprecate As this is still a WIP RFC, I'll update it tomorrow to reflect the name change. Personally, I feel it might be best to make |
Sure – so rename it to something like I think that's something like what I was getting at earlier. I think it would be great – it ends up at what we were asking for in the ESLint discussion on |
^ that would be super sweet BTW. It'd be exactly what we wanted. ✨ |
I've updated the original post slightly but I'm a bit tired, so let me know if there's anything I've missed. I used your example too :) |
Looks great! Two small notes:
|
What's the current thinking here? Would it make sense to make this an RFC? |
It would, maybe merge with the createRef RFC maybe. I’m not sure if I’ll get the time to start another RFC to do such things right now, I’m pretty busy working on the compilation work. Maybe someone else would like to push this through. |
I can write this up as an RFC. I'm going to follow along on #11973 for a bit first, though. Does the OP here still reflect your thinking on this matter? |
It does, although we need to incrementally move to a better ref system rather do we’re not surprising users. I believe adding createRef() and deprecated string refs is a good first step towards getting to that point. |
@trueadm I believe just copying PR can be a good RFC. I got it purpose. |
@trueadm regarding your comment example, is there any other way to achieve this without
|
@sag1v For this kind of api you should provide callback ref in children arguments. class ExternalClick extends React.Component {
state = {
clickedOutside: false
}
registerChild = element => {
this.ref = element;
if (element) {
document.addEventListener('mousedown', this.handleClickOutside);
} else {
document.removeEventListener('mousedown', this.handleClickOutside);
}
}
handleClickOutside = e => {
if (this.ref && this.ref.contains) {
const clickedOutside = !this.ref.contains(e.target);
this.setState({ clickedOutside });
}
}
render() {
const { children } = this.props;
const { clickedOutside } = this.state;
const { registerChild } = this;
if (typeof children === 'function') {
return children({ registerChild, clickedOutside });
} else {
return null
}
}
} |
@TrySound Thanks! |
Seems like |
@gaearon The remaining question is the more ambitious one of "Deprecate the "ref" prop entirely" – are we convinced now that this is unnecessary? |
I don’t think we really considered doing this. It’s important to have an imperative escape hatch, I think, and it doesn’t necessarily have to be tied to host instances. Any proposal for completely removing refs for custom components would need to consider a broad set of use cases, and provide alternatives for each of them. I don’t think this proposal has done it. |
@gaearon I am sorry, maybe I don't understand correctly the documentation and usage, but how |
@naorye @gaearon |
This is a formal discussion to talk about the future of refs within React and how we can improve upon them.
Current Behavior
Currently, there are two ways of doing refs in React, string refs and callback refs.
String refs
String refs can be applied to "composite" components that are class components (i.e.
<MyComponent />
) and "host" components (i.e.<span />
).An example of how this might look like for both types:
Callback refs
Callback refs can also be applied to "composite" components that are class components (i.e.
<MyComponent />
) and "host" components (i.e.<span />
).An example of how this might look like for both types:
Proposed Behavior
I propose three major changes to how the current ref system works:
Deprecate string refs for removal in React 17
The ref API is broken is several aspects (taken from #1373).
<Child renderer={index => <div ref="test">{index}</div>} />
-- this ref will be attached where the callback is issued, not in the current owner.Callback refs do not have the above issues and have been the recommended choice by the React team for some time. You can already do everything and more with callback refs, so I personally feel there's no need to keep the string ref system around.
Other libraries, such as Inferno and Preact have already removed string refs and have reported performance optimization from doing so.
Deprecate the "ref" prop entirely
I feel refs on components lead to problematic patterns that make apps much harder to scale because it can easily break the uni-direction flow of a component tree. In my opinion, class components shouldn't be able to access the instances of other components for communication – they should use
props
instead. Alternatively, in cases where access of a root DOM node is needed but unavailable, a wrapper component (#11401 (comment)) could be used as an escape hatch.The below example is something that I personally feel is a problematic pattern and one that I've seen bite teams in the past:
The above example couples all the handling of the items in the item container, breaking the control flow. Ideally, the
SubscriptionHandler
should be passed to the child as a prop, then the child can control its own flow.Another usage of refs on composite components is related to
ReactDOM.findDOMNode(...)
usage. By passingfindDOMNode
the component instance from the ref, you can get back the root DOM node. An example of this follows:This approach can be avoided in this instance by passing refs via props:
Add a special "hostRef" prop that only works on host components
This is to reduce confusion, as
hostRef
would be a normal prop on composite components. Keeping the current "ref" naming might cause unintended problems. This would also allow apps to move over to the new system incrementally. Furthermore,hostRef
should only accept callback refs, not string refs. An example of this:Downsides
Migration Cost
Both changes in this proposal have a cost for migration.
Codemodding
It may be possible to automate the vast majority of string refs to callback refs via a codemod. There will need to be some form of checking for where the owner of a ref differs in cases of string refs vs callback refs. [This point needs to be broken apart and discussed more]
It might not be possible to automate a codemod for refs on composite components as it would require a change in how the structure of the components in an app work. [This point needs to be broken apart and discussed more]
Other Considerations?
React Native currently doesn't have host components, only composite components. So refs on core components such as
<View />
will need special consideration for how they may function as they do now. Maybe they could function by a prop calledviewRef
or something similar, which would work like refs currently do.The text was updated successfully, but these errors were encountered: