React source code analysis - API series - react createRef/forwardRef

Posted by c4onastick on Mon, 07 Feb 2022 13:54:15 +0100

preface

In this article, we will continue to analyze the two APIs of createRef() and forwardRef() in react. First, we will talk about the concept of ref and how it is used in react, and then analyze the source code implementation of these two methods in react.

text

First, let's take a look at the basic usage and some concepts of ref in react. Of course, if you are familiar with it, skip and look at the source code directly!

About ref

In fact, ref is the abbreviation of reference, which is a reference. Let's take a look at the important concepts and usage of ref in react

Origin of ref

In the typical react data flow, or the data flow mode advocated by react, props is the only way for parent components to interact with child components. To modify a subcomponent, you need to re render it with the new props, but in some cases, you need to force the modification of subcomponents / elements outside the typical data flow.

Let's take a look at the usage of react ref:

It is commonly used before react 16.3. Generally, ref can be obtained through binding string (Ref) or callback function (Ref).

string ref

// string ref

class App extends React.Component {
    componentDidMount() {
        this.refs.myRef.focus()
    }

    render() {
        return <input ref="myRef" />
    }
}

This is the most primitive way of using ref, but this way is about to be eliminated, and the official also makes it clear:

If you are still using this refs. Textinput accesses refs in this way. We suggest using callback function or createRef API instead.

Refer to this article for specific disadvantages Understand React ref , it's fairly clear.

callback ref

// callback ref

class App extends React.Component {
    componentDidMount() {
        this.myRef.focus()
    }

    render() {
        return <input ref={ele => this.myRef = ele} />
    }
}

Let's talk about the understanding of callback ref here, because it will involve some comparison with createRef. It's better to understand it.

In fact, react will call the callback function of ref and pass in the target dom element before mounting the component. It will also be called again when unloading the component, but the parameter becomes null at this time. Before ComponentDidMount and ComponentDidUpdate, react will ensure that all refs are up-to-date.

The use of callback ref will also affect the performance of react. If we define the form of inline function directly on the dom element, the callback function will be executed twice in the process of component update. Because the react update component is equivalent to uninstalling and then mounting, each time the react is updated, you must first pass in null to empty the old component (the first time), and then pass in the dom element to mount the new function instance (the second time).

We can avoid this problem by binding the ref callback function to the class, but it doesn't matter in most cases. On the whole, callback ref is a recommended method with good performance.

createRef

After react 16.3, the new proposal introduces a new api, which is the protagonist of this article - createRef. Let's see how to use it:

// createRef

class App extends React.Component {
    constructor(props) {
        super(props)
        this.myRef = React.createRef()
    }
    
    componentDidMount() {
        this.myRef.current.focus()
    }

    render() {
        <input ref={this.myRef} />
    }
}

The advantage of createRef is that it is more intuitive than callback ref and avoids some problems in the understanding of callback functions. The disadvantage of createRef is that its performance is slightly lower than that of callback Ref.

The value of ref created by createRef will vary according to the node type:

When the ref attribute is used for an HTML element, react is used in the constructor The ref created by createref() receives the underlying DOM element as its current attribute.

  • When the ref attribute is used for a custom class component, the ref object receives the mounted instance of the component as its current attribute.

  • By default, you cannot use ref attributes on function components (which can be used inside function components) because they have no instances.

  • If you want to use ref in a function component, you can use forwardRef (which can be used in combination with useImperativeHandle) or you can convert the component into a class component. Next, we will also learn about forwardRef.

useRef

We have learned three basic ways to use ref above. We can easily get the reference of dom elements and make some modifications or actions. However, we should know that we can't use ref in the above three ways in class components, because the above three ways are actually built in stateful components, and we can see that this appears in all of them, but in fact, there is no concept of this in function components, and there are no examples.

useRef is actually a hook. In fact, more and more functional components are advocated to cooperate with react hooks. You can have a look at hooks Hooks in React - a complete guide , very detailed. I won't start here.

forwardRef

Before officially entering the source code analysis, you still need to know how to use it (nonsense). Take the following example:

function App() {
    const inputEl = useRef(null)
    const btnClick = () => {
        inputEl.current.focus()
    }

    return (
        <>
            <input ref={inputEl} type="text"/>
            <button onClick={btnClick}>focus</button>
        </>
    )
}

There's nothing to show. In short, pressing a button can trigger the focus event of the input box.

When we click button, first create a ref object inputEl through useRef, then assign inputEl to the ref of input, and finally, through inputEl current. Focus () can focus input.
Then, let's think about what to do if input is not an ordinary dom element, but a component?

This involves another api - forwardRef.

By modifying the above code, we can package the input into a component through forwardRef:

const InputWithFocusButton = React.forwardRef((props, ref) => {
    return <input ref={ref} type="text"/>
})

Let's see how to use this component:

function App() {
    const inputEl = useRef(null)
    const btnClick = () => {
        inputEl.current.focus()
    }

    return (
        <>
            <InputWithFocusButton ref={inputEl} />
            <button onClick={btnClick}>focus</button>
        </>
    )
}

You can see react Forwardref accepts a rendering function that receives props and ref parameters and returns a ReactNode.
In this way, we can forward the ref created in the parent component to the child component, assign it to the input element of the child component, and then call its focus method.

See here, through useRef + forwardRef, we can use ref in functional components. Let's stick to it again. Next, we will introduce the last important knowledge point, useImperativeHandle, which combines the above two APIs to make your code more perfect.

useImperativeHandle

useImperativeHandle is actually a hook, but it was missed when learning hooks before.

Sometimes, we may not want to expose the whole sub component to the parent component, but only expose the values or methods required by the parent component, which can make the code more explicit. The api useImperativeHandle helps us do this.

useImperativeHandle(ref, createHandle, [deps])
  • Ref: defines the ref of the current object
  • createHandle: a function whose return value is an object, that is, the event that the sub component wants to expose
  • Object [deps]: that is, the dependency list. When the monitored dependency changes, useImperativeHandle will re output the instance attribute of the child component to the parent component
  • On the current attribute of ref, if it is an empty array, it will not be re output.

Continue with the example of forwardRef above to see how to cooperate with useImperativeHandle and why it is better:

import React, { useRef, useImperativeHandle } from 'react';
import ReactDOM from 'react-dom';

const FancyInput = React.forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));

  return <input ref={inputRef} type="text" />
});

const App = props => {
  const fancyInputRef = useRef();

  return (
    <div>
      <FancyInput ref={fancyInputRef} />
      <button
        onClick={() => fancyInputRef.current.focus()}
      >focus</button>
    </div>
  )
}

ReactDOM.render(<App />, root);

In the above example, unlike direct forwarding ref, direct forwarding ref is to connect react The ref parameter on the function in forwardref is directly applied to the ref attribute of the return element. In fact, the parent and child components refer to the current object of the same Ref. it is officially not recommended to use such ref transparent transmission and sharing.

After using useImperativeHandle, the parent and child components can have their own refs through react Forwardref transparently transmits the ref of the parent component, and defines the current opened to the parent component through the useImperativeHandle method.

The first parameter of useImperativeHandle is the ref that defines the current object, the second parameter is a function, and the return value is an object, that is, the current object of this Ref. In this way, you can customize the ref of the parent component to use some methods of the sub component ref, as in the above case.

React.createRef

The above describes so many uses of ref that you can finally enter react The source code of createref is analyzed.

Source code

  • Source address: https://github.com/facebook/react/blob/master/packages/react/src/ReactCreateRef.js
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 * @flow
 */

import type {RefObject} from 'shared/ReactTypes';

// an immutable object with a single mutable value
export function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  if (__DEV__) {
    Object.seal(refObject);
  }
  return refObject;
}

analysis

RefObject

In fact, as a whole, the logic of createRef is relatively simple, which is to return a RefObject with current attribute and the class is RefObject. Of course, we should also take a look at what RefObject is

export type RefObject = {|
  current: any,
|};

In fact, it's nothing, just defining a type.

React.forwardRef

Source code

  • Source address: https://github.com/facebook/react/blob/master/packages/react/src/ReactForwardRef.js
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import {REACT_FORWARD_REF_TYPE, REACT_MEMO_TYPE} from 'shared/ReactSymbols';

export function forwardRef<Props, ElementType: React$ElementType>(
  render: (props: Props, ref: React$Ref<ElementType>) => React$Node,
) {
  if (__DEV__) {
    if (render != null && render.$$typeof === REACT_MEMO_TYPE) {
      console.error(
        'forwardRef requires a render function but received a `memo` ' +
          'component. Instead of forwardRef(memo(...)), use ' +
          'memo(forwardRef(...)).',
      );
    } else if (typeof render !== 'function') {
      console.error(
        'forwardRef requires a render function but was given %s.',
        render === null ? 'null' : typeof render,
      );
    } else {
      if (render.length !== 0 && render.length !== 2) {
        console.error(
          'forwardRef render functions accept exactly two parameters: props and ref. %s',
          render.length === 1
            ? 'Did you forget to use the ref parameter?'
            : 'Any additional parameter will be undefined.',
        );
      }
    }

    if (render != null) {
      if (render.defaultProps != null || render.propTypes != null) {
        console.error(
          'forwardRef render functions do not support propTypes or defaultProps. ' +
            'Did you accidentally pass a React component?',
        );
      }
    }
  }

  const elementType = {
    $$typeof: REACT_FORWARD_REF_TYPE,
    render,
  };
  if (__DEV__) {
    let ownName;
    Object.defineProperty(elementType, 'displayName', {
      enumerable: false,
      configurable: true,
      get: function() {
        return ownName;
      },
      set: function(name) {
        ownName = name;

        // The inner component shouldn't inherit this display name in most cases,
        // because the component may be used elsewhere.
        // But it's nice for anonymous functions to inherit the name,
        // so that our component-stack generation logic will display their frames.
        // An anonymous function generally suggests a pattern like:
        //   React.forwardRef((props, ref) => {...});
        // This kind of inner function is not used elsewhere so the side effect is okay.
        if (!render.name && !render.displayName) {
          render.displayName = name;
        }
      },
    });
  }
  return elementType;
}

analysis

About__ DEV__ The code for is skipped for the time being. We can see that forwardRef accepts a render function as a parameter, and this render function has two parameters, props and ref, and the return type is required to be a ReactNode.

There is a noteworthy point about the understanding of the source code in this part. Once referenced with jsx in the parent component, through react The subassembly of forwardref package is essentially equivalent to:

React.createElement(React.forwardRef(child), ...)

At this time, there is another layer of package?? typeof is REACT_ELEMENT_TYPE, type is react Forwardref (child), type?? typeof is REACT_FORWARD_REF_TYPE.

Conclusion

This article is a more detailed coverage of the various uses of ref in react. The source code is for react Createref and react Forwardref's source code analysis is relatively simple. I think ref is still very important. I also want to know more about it through this article. If there are mistakes, you are welcome to correct them, and I hope this article will help you a little ~

Topics: React