The React of forwardRef

preface

Recently I was learning React, but I didn’t know the forwardRef and Context very well. Now I finally understand it, so I want to write an article to record it.

React.forwardRef

For ref forwarding, the official website describes it like this

Ref forwarding is a technique for automatically passing a Ref through a component to one of its children. This is usually not required for components in most applications. But it can be useful for some components, especially reusable component libraries.

Refs Forward — React (docschina.org)

Now let’s cut to the chase!

ForwardRef (render) returns the React component and receives a render function with render(props, ref) as its signature. The second argument forwards the ref property it receives to the component that Render returns.

This technique is uncommon, but is particularly useful in two scenarios:

  • forwardingref Inside the componentDOM nodes
  • Forwarding in higher-order componentsref

forwardingrefInside the componentDOMnode

// App.js
import React from 'react';
import Foo from './component/Foo';

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {};
    this.input = React.createRef(); / / 1
      / / write 5
  }
  
  handleClick = (e) = > {
    const input = this.input.current;
      / / 6
    console.log(input);
    console.log(input.value);
    input.focus();
  }
  
  render() {
    return (
      <>
        <button onClick={this.handleClick}>click to get value</button>{/ * * / 2}<Foo ref={this.input}/>
      </>)}}Copy the code
// Foo.jsx
import React from 'react';
			             / / 3
const Foo = React.forwardRef((props, myRef) = > {
  return (
    <div>
      <p>. Some other nodes</p>{/ * * / 4}<input type="text" defaultValue='Ref successfully forwarded to the input node inside the Foo component' ref={myRef}/>
      <p>. Some other nodes</p>
      <p>. Some other nodes</p>
    </div>
  );
});

export default Foo;

Copy the code

Looking closely at the numbers marked in the code, this is the flow of ref forwarding:

  1. Created aref
  2. Mount it to the component that passes throughReact.forwardRefIt’s created, and notice this is crucial, and I’ll talk about it later
  3. componentFooReceived aref, and forward it toDOMnodeinputon
  4. refMount to the internal node as desiredinputon
  5. nowthis.input.currentThe pair nodes are savedinputA reference to the
  6. Click the button and it’s now easy to getFooInternal nodevalueAnd getting its focus

Details added

As mentioned earlier, step 2 is critical because the value of ref varies depending on the type of node:

  1. When the REF attribute is used on an HTML element, the underlying DOM element is received as its current attribute.

  2. When the REF attribute is used for a custom class component, ref receives the component’s mount instance as its current attribute.

  3. You cannot use ref attributes on function components because they have no instances.

The first is easy to understand, as our example above shows, where ref ends up being mounted on the input node, which is an HTML element, so the DOM element remains in current.

For the second, we now demonstrate it with another component

// Bar.jsx
import React from 'react';

export default class Bar extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      message: 'This is a class component, ref can only be mounted to an instance'
    };
  }
  
  componentDidMount() {
    console.log(this);
  }
  render() {
    return (
      <div>The class components</div>); }}Copy the code
// App.js
import React from 'react';
import Bar from './component/Bar'

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {};
    this.myRef = React.createRef(); / / create the ref
  }
  
  handleClick = (e) = > {
    const instance = this.myRef.current;
    // Print an instance of Bar
    console.log(instance);
  }
  render() {
    return (
      <div>
        <button onClick={this.handleClick}>click to get instance</button>{/* Mount to component, because Bar is a class component, it can only be mounted to instances */}<Bar ref={this.myRef} />
      </div>); }}Copy the code

The first print is from the life cycle function after the Bar component is mounted

The second print is printed after the button is clicked, proving that it is indeed only mounted to the component instance. Similar problems occur in later higher-order components.

For the third, you cannot use the ref attribute on function components because they have no instances

// Baz.jsx
import React from 'react';

const Baz = (props) = > {
  return (
    <div>Ah?</div>
  );
};

export default Baz;

Copy the code
// in app.js, other code is omitted

<Baz ref={this.myRef} />
Copy the code

An error is reported, meaning ref cannot be used on functional components, and attempts to access ref will fail.

ForwardRef (forwardRef) {react. forwardRef (forwardRef);}} function (ref) {forwardRef (forwardRef)}}};}};

// Transform our functional component to look like this.
const Baz = React.forwardRef((props, ref) = > {
  return (
    <div>Ah?</div>
  );
})
Copy the code

Ref. Current is not yet mounted, so calling ref. Current will return null, but it will not return an error

Functional components simply pass the ref, which can only be mounted on a class component or HTML element.

As an additional note, the inability to use the REF attribute on functional components does not mean that you cannot use ref inside functional components

As follows:

/ / or Baz. JSX
import React from 'react';
const myRef = React.createRef();

const Baz = (props) = > {
  function handleClick(e) {
    const input = myRef.current.input;
    console.log(input.value);
  }
  return (
    <div>
      <button onClick={handleClick}>click to get value</button>
      <input type="text" defaultValue={'cannot be used on function componentsrefAttribute does not mean that 'cannot be used inside functional componentsref` '} / >
    </div>
  );
}

export default Baz;

Copy the code

Forwarding in higher-order componentsref

Use the same bar.jsx as before

// Bar.jsx
import React from 'react';

export default class Bar extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      message: 'this is the Bar. The JSX'
    };
  }
  
  componentDidMount() {
    console.log(this);
  }
  render() {
    return (
      <div>The class components</div>); }}Copy the code

We used higher-order components, adding a function to the Bar component that prints its changes every time its props changes

// logProps.js
function logProps(WrappedComponent) {
    
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('Previous props: ', prevProps);
      console.log('Current props: '.this.props);
    }
    render() {
        // The higher-order component passes through all props to its wrapped component, so the render result will be the same
      return <WrappedComponent {. this.props} / >; }}return LogProps;
}
Copy the code

But higher-order components don’t! Will! Pass! Pass! Ref, because ref is not a prop property. Just like the key, it gets special processing by React.

If you add a REF to a component wrapped by a higher-order component, the ref will refer to the outermost container component, not the wrapped component.

For the example above, if ref was used, it would end up being mounted on the
component instead of the wrapped
component that was passed in.

If you simulate the data flow in your head, the ref will eventually be mounted to the outermost component. However, the concept of pass-through in higher-order components is very easy to mislead people into thinking that the REF will pass through along with the props, which it will not.

To make it easier to see which component is being mounted to on the console, we add state for both components

/ / Bar. The JSX
this.state = {
  message: 'this is the Bar. The JSX'
}

// The logProps component returned in logProps
this.state = {
  message: 'this is LogProps'
}
Copy the code
// App.js
import React from 'react';
import logProps from './component/logProps';
import Bar from './component/Bar'

const BarWithLogProps = logProps(Bar);
export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: 'Roman'.age: 23.hobby: 'video game'
    }
    this.myRef = React.createRef();
  }
  handleClick = (e) = > {
    this.setState({
      name: 'fxy'.age: 32.hobby: 'swim'
    });
    console.log(this.myRef.current);
  }
  render() {
    return (
      <div>
        <button onClick={this.handleClick} >click to change props</button>
        <BarWithLogProps {. this.state} ref={this.myRef} />
      </div>); }}Copy the code

When we click the button, it is clear from the console that ref is indeed mounted to the external component LogProps.

The React. ForwardRef comes up again and we can use the React. ForwardRef API to explicitly forward the ref to the internal
component

Finally, we changed the logProps. Js to look like this

// Final form
import React from 'react';

export default function logProps(WrappedComponent) {
  
  class LogProps extends React.Component {
    / / 2
    constructor(props) {
      super(props);
      this.state = {
        message: 'this is LogProps'}}componentDidUpdate(prevProps) {
      console.log('Previous props: ', prevProps);
      console.log('Current props: '.this.props);
    }
    render() {
      / / 3
      const{customRef, ... props} =this.props;
      // 3.5 return 
      ;
      return <WrappedComponent {. props} ref={customRef} />; }}// return LogProps;
  return React.forwardRef((props, ref) = > (
    / / 1
    <LogProps {. props} customRef={ref} />))}Copy the code
  1. We will beLogPropsComponent asrenderFunction, so that the render result is still the same, and then passed inrefForwarded to theLogPropsComponent’s custom propertiescustomRefOn. Note that thereBe sure to forward to custom properties, if forwarded torefProperties will eventually be mounted toLogPropsUp, is equal to a circle back to the same place…
  2. All the attributes are passed topropsIn the
  3. willpropsIn thecustomRefExtract it and eventually pass it toWrappedComponenttherefOn the property.

The ref is successfully forwarded to the WrappedComponent WrappedComponent.

Click the button and now go to the console

Ref is successfully mounted to the target component.

If you are careful, you may have found that there is another way to write logProps. Js forward ref, which is like 3.5, without pulling the customRef, passing the customRef through as props, but this causes a problem: CustomRef is passed as props inside the WrappedComponent before it is mounted as a ref property on any class component or DOM node.

If ref.current is accessed at this point, null is returned

But it also makes it easier to forward the DOM nodes that ref inside the component

// Bar.jsx
render() {
  return (
    <div ref={this.props.customRef}>
      name: {this.props.name}
      <br/>
      age: {this.props.age}
      <br/>
      hobby: {this.props.hobby}
    </div>
  );
}
Copy the code

The react. forwardRef function also forwards the ref to the DOM node inside the component.

The last

There is no final, the Context back to fill it 😁, did not expect to write this quite time-consuming, I want to quickly learn the back, everyone refuelling!