React is known for abstracting complex DOM operations into simple state and props operations through declarative rendering. It was a huge hit and saved front-end engineers from noodlelike DOM operations overnight. Despite our emphasis on avoiding DOM manipulation in React development, there are some scenarios where it can’t be avoided. React doesn’t block the path, of course; it provides ref access to DOM elements or React component instances created in the Render method.

The ref’s troika

Before React V16.3, a ref was retrieved as a string ref or callback ref. In React V16.3, a ref was retrieved as a callback ref. The new React. CreateRef API was introduced with the 0017-new-create-ref proposal.

Note: The following code examples and source code are based on or derived from the React V16.3.2 release.

// string ref
class MyComponent extends React.Component {
  componentDidMount() {
    this.refs.myRef.focus();
  }
  render() {
    return <input ref="myRef" />;
  }
}

// callback ref
class MyComponent extends React.Component {
  componentDidMount() {
    this.myRef.focus();
  }
  render() {
    return <input ref={(ele) => {
      this.myRef = ele;
    }} />;
  }
}

// React.createRef
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  componentDidMount() {
    this.myRef.current.focus();
  }
  render() {
    return <input ref={this.myRef} />;
  }
}
Copy the code

String ref regression

Callback ref: Callback ref: callback ref: callback ref: callback ref: callback ref: callback ref: callback ref: callback ref The main reasons are as follows:

  • When ref is defined as string, React needs to track the components currently being rendered. During the Reconciliation phase, React Element creation and update, ref is encapsulated as a closure and waits for the commit phase to execute. This has some impact on React performance.
function coerceRef(returnFiber: Fiber, current: Fiber | null, element: ReactElement,) {... const stringRef =' ' + element.ref;
  // Get an example from fiber
  let inst = ownerFiber.stateNode;
  
  // ref closure function
  const ref = function(value) {
    const refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs;
    if (value === null) {
      delete refs[stringRef];
    } else{ refs[stringRef] = value; }}; ref._stringRef = stringRef;returnref; . }Copy the code
  • When using render callback mode, using string ref creates an ambiguity about the mount position of ref.
class MyComponent extends Component {
  renderRow = (index) = > {
    // String ref will be mounted on DataTable this
    return<input ref={'input-' + index} />; Return <input ref={input => this['input-' + index] = input} />; } render() { return <DataTable data={this.props.data} renderRow={this.renderRow} /> } }Copy the code
  • String refs cannot be combined. For example, if a parent component of a third-party library has already passed a ref to a child component, we cannot add a ref to the child component. Callback ref is a perfect solution to this problem.
/** string ref **/
class Parent extends React.Component {
  componentDidMount() {
    // Get this.refs.childref
    console.log(this.refs);
  }
  render() {
    const { children } = this.props;
    return React.cloneElement(children, {
      ref: 'childRef'}); }}class App extends React.Component {
  componentDidMount() {
    // this.refs.child cannot be fetched
    console.log(this.refs);
  }
  render() {
    return( <Parent> <Child ref="child" /> </Parent> ); } /** Parent extends React.Component {componentDidMount() console.log(this.childRef); } render() { const { children } = this.props; return React.cloneElement(children, { ref: (child) => { this.childRef = child; children.ref && children.ref(child); }}); }} Class App extends componentDidMount() {componentDidMount(); } render() { return ( <Parent> <Child ref={(child) => { this.child = child; }} /> </Parent> ); }}Copy the code
  • Does not take effect when used on the root component.
ReactDOM.render(<App ref="app" />, document.getElementById('main')); 
Copy the code
  • Less friendly to static types, when using string ref, you must explicitly declare the type of refs and cannot do automatic derivation.

  • The compiler cannot confuse a string ref with its corresponding attribute on the refs, whereas with callback ref, it can be confused.

createRef vs callback ref

The new createRef has no overwhelming advantage over callback ref, but is expected to be a convenient feature with a slight performance advantage. Callback Ref uses the mode of assigning ref in the closure function during component render. CreateRef uses object Ref.

CreateRef is more intuitive, similar to string ref, and avoids some of the problems of understanding callback ref. For callback ref we usually use the form of an inline function, which will be recreated every time we render. Because react will clean up the old ref and set the new one (commitDetachRef -> commitAttachRef), it will be called twice during the update. The first time will be null. This can be avoided by defining and binding callback as a class member function.

class App extends React.Component {
  state = {
    a: 1}; componentDidMount() {this.setState({
      a: 2}); } render() {return (
      <div ref={(dom)= >{// output three times //<div data-reactroot></div>
        // null
        // <div data-reactroot></div>console.log(dom); }} ></div>); }}class App extends React.Component {
  state = {
    a: 1};constructor(props) {
    super(props);
    this.refCallback = this.refCallback.bind(this);
  }
  
  componentDidMount() {
    this.setState({
      a: 2}); } refCallback(dom) {// Output only 1 time
    // <div data-reactroot></div>
    console.log(dom);
  }
  
  render() {
    return (
      <div ref={this.refCallback}></div>); }}Copy the code

Admittedly, createRef is not as powerful as callback Ref, and createRef can’t do anything about composition problems, such as those mentioned in the previous section. In React V16.3, string ref/callback ref is handled slightly differently than createRef.

// markRef references are compared between old and new ones
if(current.ref ! == workInProgress.ref) { markRef(workInProgress); }// effectTag is bit-based, which has the change flag bit of ref
function markRef(workInProgress: Fiber) {
  workInProgress.effectTag |= Ref;
}
  
// The & between effectTag and Ref indicates that the current fiber has a Ref change
if (effectTag & Ref) {
  commitAttachRef(nextEffect);
}

function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if(ref ! = =null) {
    const instance = finishedWork.stateNode;
    let instanceToUse;
    switch (finishedWork.tag) {
      // The current Host environment is DOM environment, HostComponent is DOM element, need to use the instance to obtain the native DOM element
      case HostComponent:
        instanceToUse = getPublicInstance(instance);
        break;
      // For ClassComponent, etc., just return the instance
      default:
        instanceToUse = instance;
    }
    // Both string ref and callback execute the ref closure
    // createRef will hang directly on the object ref's current
    if (typeof ref === 'function') {
      ref(instanceToUse);
    } else{ ref.current = instanceToUse; }}}Copy the code

React Fiber is an example of how the React Fiber object is defined, how the Fiber Tree is constructed and updated, how the effectTag is defined and how it is collected, etc. If you are not familiar with the above details, you may want to skip this section for the time being.

Wearing a cloud arrow React forwardRef

In addition to createRef, React16 also provides a react. forwardRef API for getting the refs of child elements directly through the parent element. Before we talk about the use of forwardRef, let’s review the problem with HOC (higher-order Component) refs. HOC refs cannot be passed through props. Therefore, the WrappedComponent cannot be obtained directly and needs to be transferred.

function HOCProps(WrappedComponent) {
  class HOCComponent extends React.Component {
    constructor(props) {
      super(props);
      this.setWrappedInstance = this.setWrappedInstance.bind(this);
    }
    
    getWrappedInstance() {
      return this.wrappedInstance;
    }

    // implement ref access
    setWrappedInstance(ref) {
      this.wrappedInstance = ref;
    }
    
    render() {
      return<WrappedComponent ref={this.setWrappedInstance} {... this.props} />; } } return HOCComponent; } const App = HOCProps(Wrap); <App ref={(dom) => {// // WrappedComponent console.log(dom.getwrappedInstance ()); }} / >Copy the code

Once you have the forwardRef, you don’t need to go through the getWrappedInstance; the forwardRef can go through the HOCComponent to get the WrappedComponent.

function HOCProps(WrappedComponent) {
  class HOCComponent extends React.Component {
    render() {
      const{ forwardedRef, ... rest } =this.props;
      return<WrappedComponent ref={forwardedRef} {... rest} />; } } return React.forwardRef((props, ref) => { return <HOCComponent forwardedRef={ref} {... props} />; }); } const App = HOCProps(Wrap); <App ref={(dom) => {// WrappedComponent console.log(dom); }} / >Copy the code

The forwardRef creates a special Component of the React Component. When the create update operation is performed, props and ref on the forwardRef component are passed directly to the pre-injected Render function to generate children.

const nextChildren = render(workInProgress.pendingProps, workInProgress.ref);
Copy the code

React.createRef and the React. ForwardRef API have been introduced in React16. If your application has been upgraded to Act 16.3+, feel free to use React.createRef, or callback ref instead of String ref if not yet available.

Our team is currently working on React16, and you are welcome to join us. If you want to join us, please chat privately or send your resume to [email protected].