React forwardRef

React uses the forwardRef to forward a ref.

React forwardRef

When the forwardRef is not used, the parent passes in the child’s ref property, which points to the child itself. As follows:

class Child extends React.Component {
  render() {
    return (
      <div>
        <button>Am I</button>
      </div>); }}function App() {
  const child = useRef<any>();

  useEffect(() = > {
    setTimeout(() = > {
      console.log(child);
    }, 2000); } []);return (
    <div styleName="container">
      <Child ref={child} />
    </div>
  );
}
Copy the code

A screenshot of the console at this point looks like this:

But what if I want the child to point to the child’s button? At this point, we can only expand the props of Child and add a new field like buttonRef, as shown below:

interface IProps {
  buttonRef: any;
}

class Child extends React.Component<IProps> {
  render() {
    return (
      <div>
        <button ref={this.props.buttonRef}>Am I</button>
      </div>); }}function App() {
  const child = useRef<any>();

  useEffect(() = > {
    setTimeout(() = > {
      console.log(child);
    }, 2000); } []);return (
    <div styleName="container">
      <Child buttonRef={child} />
    </div>
  );
}
Copy the code

The child refers to the DOM object, button.

You can see the problem in this scenario: the child component needs to extend the buttonRef field.

React provides the forwardRef for forwarding refs. This way the component does not have to extend the extra REF field while providing the internal DOM

The modified code is as follows:

const Child = forwardRef((props: any, ref: any) = > {
  return (
    <div>
      <button ref={ref}>Am I</button>
    </div>
  );
});
function App() {
  const child = useRef<any>();

  useEffect(() = > {
    setTimeout(() = > {
      console.log(child);
    }, 2000); } []);return (
    <div styleName="container">
      <Child ref={child} />
    </div>
  );
}
Copy the code

After modification, App components can still use ref as before.

Use the forwardRef component to forward the Ref

This example is an example from the official documentation. Readers who have read it can skip to three.

There is a Hoc component for printing props:

function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:'.this.props);
    }

    render() {
      return <WrappedComponent {. this.props} / >; }}return LogProps;
}
Copy the code

When it is applied to a child component, the ref will fail because there is a layer between the two, resulting in a parent component calling ref.current.xxx() failing:

import Button from './Button';
const LoggedButton = logProps(Button);

const ref = React.createRef();

// The LoggedButton component is HOC LogProps.
// Although the render result will be the same,
// But our ref will point to the LogProps instead of the internal Button component!
// This means that we cannot call methods such as ref.current. XXX ()
<LoggedButton label="Click Me" handleClick={handleClick} ref={ref} />;
Copy the code

Then we need to wrap another layer with the forwardRef in hoc and forward the ref:

function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:'.this.props);
    }

    render() {
      const{ componentRef, ... rest } =this.props;
      return <WrappedComponent {. rest} ref={this.props.componentRef} />; }}return forwardRef((props, ref) = > {
    return <LogProps {. props} componentRef={ref} />;
  });
}
Copy the code

Extension: use the forwardRef and useImperativeHandle to restrict the parent from calling the parent’s Api

Suppose a developer has developed a button.tsx component that controls a state switch. The component code is as follows:

import React from 'react';

export enum Status {
  On = 'on',
  Off = 'off',
}

interface IProps {
  onChange: (status: Status) = > void;
}

interface IState {
  status: Status;
}

class Button extends React.Component<IProps.IState> {
  state = {
    status: Status.Off,
  };

  private logStatus = () = > {
    console.log(this.state.status);
  };

  onToggleStatus = () = > {
    this.setState(
      (prevState) = > ({
        status: prevState.status === Status.Off ? Status.On : Status.Off,
      }),
      () = > {
        this.logStatus();
        this.props.onChange(this.state.status); }); };render() {
    return (
      <div>
        <button onClick={this.onToggleStatus}>status: {this.state.status}</button>
      </div>); }}export default Button;
Copy the code

The demo effect is as follows, each click to switch the internal state:

The Button component provides an onChange callback. External components can pass in the onChange method to get real-time status, while internal components control the status through onToggleStatus.

Now if another developer is working on an external component and wants to implement the second Button externally in real time and synchronously display the state of the Button. At this point, he can use onChange to synchronize the state in real time, and there are generally two ways to modify the Button state externally:

  1. Change the Button component to a pure function component and promote its state and the method to modify the state to the parent component or state management tool.
  2. Get the component through ref, passref.current.onToggleStatus()To modify the child component state.

Both methods have advantages and disadvantages. Users need to choose them based on actual scenarios. Now I will discuss how to better use the second scheme.

First, list the code for the parent component that uses ref:

function App() {
  const [buttonStatus, setButtonStatus] = useState<Status>();
  const ref = useRef<any>();

  const onClick = useCallback(() = > {
    // Update internal status
    ref.current.onToggleStatus();
    // Update the external state
    setButtonStatus(() = >ref.current.state.status); } []);// Synchronize state for the first rendering
  useEffect(() = > {
    setButtonStatus(() = >ref.current.state.status); } []);return (
    <div styleName="container">
      <Button ref={ref} onChange={setButtonStatus} />
      <button onClick={onClick}>Control and display 👆 buttonStatus: {buttonStatus}</button>
    </div>
  );
}
Copy the code

The parent component reads the Button state through ref.current. State. status, and synchronizes the state in real time through the onChange hook.

At this point, the functionality is actually developed. However, in the spirit of exploration, a review of the code and invocation of the parent component reveals several problems with this approach:

  1. The interface of the child component Button is completely exposed, and if the child component is a multicomponent reusable component whose state is integrated into the state management tool, it is quite risky for the parent component to call it at will.
  2. The developers of the Button sub-component did not have the development awareness to design interfaces for external active invocation in their actual development. Using Button component as a pastry chef, onChange is a responsive API style, which is how many cakes I make, when I make, when you eat. The interface for external active invocation is an imperative API style, more like external active ordering, I’ll cook as much as you order, and WHEN you order, I’ll cook it for you.

To solve the above problems, we can adopt the following optimization methods: use forwardRef and useImperativeHandle to encapsulate a HOC, dynamically supplement the interface for external active invocation, and make the Button component unconscious.

The code is as follows:

import React, { useImperativeHandle, useRef } from 'react';

function buttonDecorator(Component: any) {
  const WrappedComponent: React.FC<any> = (props) = > {
    const childRef = useRef<any>();
    const{ parentRef, ... rest } = props;// Encapsulates an interface for external active invocation
    useImperativeHandle(parentRef, () = > ({
      toggleStatus: () = > {
        childRef.current.onToggleStatus();
      },
      getStatus: () = > {
        returnchildRef? .current? .state? .status; }}));return <Component {. rest} ref={childRef} />;
  };

  return React.forwardRef<any, any>((props, ref) = > {
    return <WrappedComponent {. props} parentRef={ref} />;
  });
}

export default buttonDecorator;
Copy the code

You can see that hoc wrapping the Button provides only toggleStatus and getStatus methods. The hoc uses useImperativeHandle to inject the forwarded parentRef with a public API, which implements calls to child components through childRef. The parent component calls ref.current. State to access the state, and an error is reported:

For the first problem (reuse of multiple components may cause side effects), developers can define their own API using useImperativeHandle to control the scope of side effects to the state management tool. For the second problem (component internal API tends to be more responsive style onXX, external calls tend to be more imperative style), Developers can make the API look imperative from the outside by simply naming it twice, such as replacing onToggleStatus with toggleStatus. The parent component needs to be modified:

  const onClick = useCallback(() = > {
    // Update internal status
    ref.current.toggleStatus();
    // Update the external state
    setButtonStatus(() = >ref.current.getStatus()); } []);// Synchronize state for the first rendering
  useEffect(() = > {
    setButtonStatus(() = >ref.current.getStatus()); } []);Copy the code

Four,

This paper mainly introduces the significance of forwardRef, how forwardRef forwards refs in hoc and an actual business scenario of forwardRef. More scenarios of how to use forwardRef need to be explored by the reader, and hopefully this article will give you some inspiration.