-
Notifications
You must be signed in to change notification settings - Fork 49.5k
Description
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:
// host components
class MyComponent extends React.Component {
componentDidMount() {
this.refs.input.focus();
}
render() {
return <div><input ref="input" type="text" /></div>
}
}
// composite components
class InputWrapper extends React.Component {
focus() {
this.refs.input.focus();
}
render() {
return <input ref="input" type="text" />
}
}
class FormComponent extends React.Component {
componentDidMount() {
this.refs.inputWrapper.focus()
}
render() {
return <InputWrapper ref="inputWrapper" />
}
}
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:
// host components
class MyComponent extends React.Component {
componentDidMount() {
if (this._inputNode) {
this._inputNode.focus();
}
}
render() {
return (
<div>
<input ref={domNode => this._inputNode = domNode} type="text" />
</div>
);
}
}
// composite components
class InputWrapper extends React.Component {
focus() {
this._input.focus();
}
render() {
return <input ref={domNode => this._input = domNode} type="text" />
}
}
class FormComponent extends React.Component {
componentDidMount() {
this._inputWrapper.focus()
}
render() {
return <InputWrapper ref={instance => this._inputWrapper = instance} />
}
}
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).
- You have to refer to this.refs['myname'] as strings to be Closure Compiler Advanced Mode compatible.
- It doesn't allow the notion of multiple owners of a single instance.
- Magical dynamic strings potentially break optimizations in VMs.
- It needs to be always consistent, because it's synchronously resolved. This means that asynchronous batching of rendering introduces potential bugs.
- We currently have a hook to get sibling refs so that you can have one component refer to it's sibling as a context reference. This only works one level. This breaks the ability to wrap one of those in an encapsulation.
- It can't be statically typed. You have to cast it at any use in languages like Flow or TypeScript.
- There's no way to attach the ref to the correct "owner" in a callback invoked by a child.
<Child renderer={index => <div ref="test">{index}</div>} />
-- this ref will be attached where the callback is issued, not in the current owner. - They require access to the React runtime to find the current owner during the creation of a ReactElement, making ahead-of-time optimizations hard to deal with.
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:
class ItemContainer extends React.Component {
render() {
let { subscribe, unsubscribe } = props.SubscriptionHandler;
return (
<ul>
{ this.props.items.map( item => (
<ListItem
key={item.uid}
data={item.data}
ref={
_ref => _ref ? subscribe(item.uid, _ref) : unsubscribe(item.uid, _ref)
}
/>
) }
</ul>
);
}
}
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 passing findDOMNode
the component instance from the ref, you can get back the root DOM node. An example of this follows:
class DOMContainer extends React.Component {
render() {
if (this.props.type === "inline") {
return <span />;
} else {
return <div />;
}
}
}
class Appender extends React.Component {
componentDidMount() {
ReactDOM.findDOMNode(this._domContainer).appendChild(this.props.node);
}
render() {
return <DOMContainer ref={_ref => this._domContainer = _ref} type="inline" />
}
}
This approach can be avoided in this instance by passing refs via props:
function DOMContainer(props) {
if (props.type === "inline") {
return <span ref={props.rootRef} />;
} else {
return <div ref={props.rootRef} />;
}
}
class Appender extends React.Component {
componentDidMount() {
this._rootRef.appendChild(this.props.node);
}
render() {
return <DOMContainer rootRef={_ref => this._rootRef = _ref} type="inline" />
}
}
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:
function Button({ className, ...props }) {
return (
<button
{...props}
className={classNames(className, 'my-btn')}
/>
);
}
// "hostRef" is a simple prop here, and gets passed through to the <button> child via JSX spread
<Button hostRef={ _ref => console.log(_ref) } className="headerBtn" />
Downsides
Migration Cost
Both changes in this proposal have a cost for migration.
- String refs are still widely used in third-party components but are likely to be trackable and upgraded via codemodding.
- Refs on composite components are far more widely used than string refs, so it may not make sense to make those changes vs the cost it will have on the React ecosystem. It's unlikely that they can be upgraded via a codemod.
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 called viewRef
or something similar, which would work like refs currently do.