preface

The emergence of front-end frameworks such as Vue, React and Angular makes it no longer necessary for us to manually manipulate the Dom. Back in the era of JQuery, dealing with Dom was the most frequent and painful. For example, one of the books I gained the most during my front-end introduction period was the art of JavaScript Dom programming.

We deal with DOM much less directly these days, but it’s still unavoidable in some scenarios, such as focusing input boxes, scrolling, text selection, and animation. In React, we have ref, createRef, and useRef. To make it easy to manipulate the DOM in unavoidable situations.

The following content requires you to know about the useRef brothers. If you haven’t already, you can skip to the first part of the text where I will explain the use of the useRef brothers in turn.

I didn’t want to write this article because the useRef brothers are so important, in fact most of the front end students rarely use it. It was the scenario I encountered that made me realize that useRef can be useful in scenarios other than DOM:

During my previous internship, I was in charge of a function configuration platform. FaaS functions needed to complete a series of configurations on this platform (the specific configuration items are not mentioned). These configurations were divided into multiple independent components, and each component could be responsible for its own data. But one day came a big change that now allowed other users (ali’s other BU) to write their own configuration components that could be seamlessly integrated into the overall configuration. In short, now I want to collect the data from the user-written component after the user completes the configuration and complete the uniform commit.

The specific ideas and scheme comparison will be directly skipped. Finally, the scheme I give is like this:

  • Provide a scaffolding that encapsulates the development specification that user-defined components need to be developed and published to an internal NPM source
  • The development specification includes two methods that must be implemented: data collectioncollectAnd data checkvalidateMethod, the former returns configuration information and metadata in the component when invoked, and the latter returns the verification status after verifying the configuration of the component
  • When configuration is complete, the main component gets the validation status of all custom child components (validateMethods) and data (collectMethod), after the verification is completed, the sub-component data is unified into the state tree for saving.

It seems simple enough, but there are a few problems:

How does the parent get the child component method:

  • That’s easy. UseuseRefThe three brothers expose the method of the child component, and the parent component passes throughrefMake a call, such asref.current.validate()

The function items to be configured (each function needs to be configured in turn) and the custom items in the user configuration are undefined. Due to the principle of hooks (do not use hooks in loops), we cannot dynamically call useRef to generate ref based on the number of functions and configuration items:

  • So just use a ref, and all the methods are mounted to that REF

If ref is written multiple times, the current property above will be overwritten, even if the key name is different:

  • Encapsulate a hook of its own, allowing each writecurrentIs used to merge values

All right, that’s it. That’s it

I call the last encapsulated hook useMultiImperativeHandle, which has a long name but is actually very simple because it is actually an enhancement to useImperativeHandle and the underlying layer is based on its implementation. In the last part, I will introduce the idea and practical use of useRef. If you are familiar with useRef, you can directly look at the useMultiImperativeHandle source code (really simple).

I recommend using my Parcel- tsx-template to run the demo in this article, which is much lighter than Webpack and create-React-app, and is good enough to handle common small projects.

useRef

In the React Class component, we created a ref using createRef.

class MyComponent extends React.Component {
  constructor(props) {
    super(props);

    this.inputRef = React.createRef();
 }   render() {  return <input type="text" ref={this.inputRef} />;  }   componentDidMount() {  this.inputRef.current.focus();  } } Copy the code

In this example ref is assigned to the native DOM element , in which case the DOM element can be retrieved via ref.current and the above method called directly. Ref can also be assigned to a Class component, so that the ref.current gets an instance of the Class component.

However, refs cannot be assigned to a functional component (unless the forwardRef is used, see the next section) because functional components have no instances.

In functional components, we use ref like this (note, “use ref in functional components”! == “Assign ref to functional component”)

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = (a)= > {
    inputEl.current.focus();
  };
 return (  <>  <input ref={inputEl} type="text" />  <button onClick={onButtonClick}>Focus the input</button>  </>  ); } Copy the code

The important difference between createRef and useRef is that createRef cannot be used in a functional component, while useRef cannot be used in a Class component. The former cannot refer to the ref created by createRef in a functional component, and its value is continuously initialized as the functional component is re-executed. The latter is simpler: hooks cannot be used on Class components.

UseRef actually has some quirks. It is often used to solve closures (see Dan’s article) and timer problems because it can hold values on current for the entire life of a component. For example, ali’s open source React Hooks library ahooks makes extensive use of useRef to save timers, as does a custom Hooks useVerifyCode I wrote earlier.

This hooks scenario is designed for common front-end capTCHA scenarios such as click send SMS – disable button for 60 seconds – Restore button click.

And this official custom hookusePrevious:

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}
Copy the code

This hooks get the last values, useEffect is executed after each render, so ref values are always the same during this render.

forwardRef

Refs cannot be assigned to functional components that are not wrapped by the forwardRefd.

The forwardRef is used like this:

const App: React.FC = () = > {
  const ref = useRef() as MutableRefObject<any>;

  useEffect(() = >{ ref.current.focus(); } []);return (
    <>
      <Child ref={ref} />
    </>
  );
};

const Child = forwardRef((props, ref: Ref<any>) = > {
  return <input type="text" name="child" ref={ref} />;
});
Copy the code

Since type is not important here, I’ll just use any

The forwardRef can wrap a functional component directly, and the wrapped functional component gets its own ref (as the second argument).

If you assign a ref to a functional component that isn’t wrapped by the forwardRef, React will give you an error on the console.

Another way to use forwardRef is to forward refs in advanced components. Since HOC is being used less and less, we don’t need to expand it here.

The forwardRef is usually used with the useImperativeHandle. If the forwardRef gives a functional component the ability to be seen by others, the useImperativeHandle is the veil on its face: it can decide what it wants you to see.

useImperativeHandle

The code in the forwardRef example is actually not recommended because there is no control over what values are exposed to the parent, so we use useImperativeHandle to control what is exposed to the parent:

Let’s look at the call signature in @types/ React:

function useImperativeHandle<T, R extends T>(ref: Ref<T>|undefined, init: () => R, deps? : DependencyList): void;Copy the code

From this signature we can probably get the call method:

  • To receive aref
  • Receives a function that returns the object to be exposed as ref
  • similaruseEffectReceives a dependency array
onst App: React.FC = () = > {
  const ref = useRef() as MutableRefObject<any>;

  useEffect(() = >{ ref.current.input.focus(); } []);return (
    <>
      <Child ref={ref} />
    </>
  );
};

const Child = forwardRef((props, ref: Ref<any>) = > {
  const inputRef1 = useRef() as MutableRefObject<HTMLInputElement>;
  const inputRef2 = useRef() as MutableRefObject<HTMLInputElement>;

  useImperativeHandle(
    ref,
    () = > {
      return {
        input: inputRef1.current,
      };
    },
    [inputRef1]
  );

  return (
    <>
      <input type="text" name="child1" ref={inputRef1} />
      <br />
      <input type="text" name="child2" ref={inputRef2} />
    </>
  );
});
Copy the code

In this example, we create two refs again inside the Child component, but we only want to expose the first one, so we use useImperativeHandle to control.

The useImperativeHandle first argument represents the ref you want to operate on, and the second argument returns the value you want to mount on the current property of the ref. You can think of it as a vertical pipe, where what you put in at the top is what you get out at the bottom. The last parameter is to update the mount when inputRef1 changes.

A simple useRef does not notify when an object is mounted; if this is required, a callback ref is used.

UseRef does not have to be used to hold DOM or Class components. It can also be used to hold timers or more generally a value that needs to remain constant throughout its lifetime. Similarly, we do not have to return ref when using useImperativeHandle, for example, we return methods defined in the child component:

function usePrevious(value) {
  const ref = useRef();
  useEffect(() = > {
    ref.current = value;
  });
  return ref.current;
}
Copy the code

This hooks get the last values, useEffect is executed after each render, so ref values are always the same during this render.

The original idea is that only the children of React can call the callbacks passed in by the parent, but sometimes the reverse is needed.

In this example, there is only one child component. Suppose we have a list, and each list item needs to mount a method, which might look like this:

import React, {
  useRef,
  forwardRef,
  MutableRefObject,
  Ref,
  useImperativeHandle,
} from 'react';

// I am often teased for my stage name... I don't want to, quq
const LIST = ['The forest does not cross'.'dome heart'.'a tea'];

type IInnerFunc = () = > string;
type IGlobalRef = {
  [key: string]: IInnerFunc;
};

const App: React.FC = () = > {
  const globalRef = useRef(null) as MutableRefObject<IGlobalRef>;

  const invokeAllMountMethod = () = > {
    constglobalObject = globalRef? .current;for (const [, method] of Object.entries(globalObject)) { method(); }};return (
    <>
      <button
        onClick={()= > {
          invokeAllMountMethod();
        }}
      >
        INVOKE
      </button>
      {LIST.map((item, idx) => (
        <Item label={item} idx={idx} key={item} ref={globalRef} />
      ))}
    </>
  );
};

const Item: React.FC<{
  label: string;
  idx: number;
  ref: Ref<any>;
}> = forwardRef(({ label, idx }, ref) = > {
  const innerMethod = () = > {
    console.log(`${label}-${idx}`);
  };

  useImperativeHandle(ref, () = >({[`method-from-${idx}`]: innerMethod,
  }));

  return <p>{label}</p>;
});
Copy the code

In this example we create a globalRef and use it in each list item component to mount methods within the child component. But run the demo and you’ll see that only the last list item method is mounted. In fact, we mentioned this earlier: with a vertical pipe, what you put in the top is what you get in the bottom, and we always have only one globalRef, so the last mount on multiple calls overrides the previous one.

We now return to the preamble scenario: how do I merge existing values with the current value at mount time?

Recall that in useImperativeHandle we mount the object returned by the init function to the current property of the original ref, whatever is returned. This means that we can get the current attribute of the original ref, so we can simply merge the previous current with the current object:

{
. originRef.current,. convertRefObj,};
Copy the code

Switching to the above example, the list items can be mounted sequentially.

useMultiImperativeHandle

Give the source code directly, because there is nothing complicated:

import { useImperativeHandle, MutableRefObject, DependencyList } from 'react';

const useMultiImperativeHandle = <T, K extends object>(
  originRef: MutableRefObject<T>,
  convertRefObj: K,
deps? : DependencyList) :void= >  useImperativeHandle(  originRef,  (a)= > {  return { . originRef.current,. convertRefObj, };  },  deps  );  export default useMultiImperativeHandle; Copy the code

Used in the example above:

const Item: React.FC<{
  label: string;
  idx: number;
  ref: Ref<IGlobalRef>;
}> = forwardRef(({ label, idx }, ref) = > {
  const innerMethod = () = > {
    console.log(`${label}-${idx}`);
  };

  useMultiImperativeHandle(ref as MutableRefObject<IGlobalRef>, {
    [`method-from-${idx}`]: innerMethod,
  });

  return <p>{label}</p>;
});
Copy the code

Done! This is the basic idea of my requirement implementation above, using a globally unique REF to mount the methods inside the component to the REF, regardless of which methods there are, and ultimately just iterate through the above methods, which then collect the component data.

True · Full text

This article doesn’t have much to offer, but it’s a simple hook that I packaged to solve the method mount of dynamic list components, and the use of the useRef brothers in functional components, as well as the method of calling child components from parent components that really makes me feel amazing. You might want to try it out. See if you can customize your own hooks based on those hooks for your own business scenario. Whether the resulting product is simple (like the main character in this article), it represents the first step in immersing yourself with the ideas of React hooks.

Related links:

  • useMultiImperativeHandle
  • useRef
  • useImperativeHandle
  • useVerifyCode
  • Parcel-Tsx-Template
  • GitHub

This article is formatted using MDNICE