Skip to content

WIP RFC: Improvements to the "ref" system #11401

@trueadm

Description

@trueadm

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions