Intro

In the React project, there are many scenarios where Ref is used. For example, use the ref attribute to get a DOM node and an instance of a ClassComponent object. Create a Ref object with the useRef Hook to solve problems like setInterval not getting the latest state. You can also manually create a Ref object by calling the React. CreateRef method.

Although Ref is also very simple to use, it is still inevitable to encounter problems in practical projects. This article will sort out various problems related to Ref from the perspective of source code, and clarify what is done behind the API related to Ref. Reading this article may give you a deeper understanding of Ref.

The type declaration associated with Ref

Ref is the abbreviation of reference. In the React type declaration file, you can find several types related to Ref, which are listed here.

RefObject/MutableRefObject

interface RefObject<T> { readonly current: T | null; }
interface MutableRefObject<T> { current: T; }
Copy the code

Using useRef Hook returned is RefObject/MutableRefObejct, these two types are defined a {current: Refobject.current is read-only. Typescript warns ⚠️ if refObject.current is modified.

const ref = useRef<string> (null)
ref.current = ' ' // Error
Copy the code

TS error: “current” cannot be assigned because it is read-only.

Look at the definition of the useRef method, where function overloading is used and RefObject

is returned if the passed generic parameter T does not contain NULL, and MutableRefObject

is returned if null is included.

function useRef<T> (initialValue: T) :MutableRefObject<T>;
function useRef<T> (initialValue: T | null) :RefObject<T>;
Copy the code

So if you want to create the current ref object attributes can be modified, need to add | null.

const ref = useRef<string | null> (null)
ref.current = ' ' // OK
Copy the code

A RefObject is also returned when the react.createref () method is called.

createRef

export function createRef() :RefObject {
  const refObject = {
    current: null};if (__DEV__) {
    Object.seal(refObject);
  }
  return refObject;
}
Copy the code

RefObject/MutableRefObject is just new in version 16.3, if you use the earlier version, you need to use the Ref Callback.

RefCallback

A Ref Callback is used to pass a Callback function. When a React Callback is called, the react Callback will pass the corresponding instance back. The type of this callback function is RefCallback.

type RefCallback<T> = (instance: T | null) = > void;
Copy the code

Using the RefCallback example:

import React from 'react'

export class CustomTextInput extends React.Component {
  textInput: HTMLInputElement | null = null;

  saveInputRef = (element: HTMLInputElement | null) = > {
    this.textInput = element;
  }

  render() {
    return (
      <input type="text" ref={this.saveInputRef} />); }}Copy the code

Ref/LegacyRef

In type declarations, there are also Ref/LegacyRef types, which are used to refer generally to Ref types. LegacyRef is a compatible version, and in previous versions ref could also be a string.

type Ref<T> = RefCallback<T> | RefObject<T> | null;
type LegacyRef<T> = string | Ref<T>;
Copy the code

Understanding the types associated with Ref makes it easier to write Typescript.

The transmission of Ref

Special props

When we use a ref on a JSX component, we do so by setting a ref to the ref property. We all know the syntax of JSX, which is compiled into createElement form by tools like Babel.

// jsx
<App ref={ref} id="my-app" ></App>

// compiled to
React.createElement(App, {
  ref: ref,
  id: "my-app"
});
Copy the code

Ref looks like any other prop, but if you try to print props. Ref inside the component is undefined. And the Dev environment console will give you a hint.

Trying to access it will result in undefined being returned. If you need to access the same value within the child component, you should pass it as a different prop.

What does React do to ref? As you can see from the ReactElement source code, ref is the RESERVED_PROPS, as is the key, which is extracted from the props and passed to the Element.

const RESERVED_PROPS = {
  key: true.ref: true.__self: true.__source: true};Copy the code

So ref is the “props” that will be handled specially.

forwardRef

Before version 16.8.0, Function Component was stateless and only acted on the props Render passed in. With hooks, you can not only have internal state, but also expose methods for external calls (with the help of the forwardRef and useImperativeHandle).

If you use a ref on a Function Component directly, the dev console will alert you to wrap it with the forwardRef.

function Input () {
    return <input />
}

const ref = useRef()
<Input ref={ref} />
Copy the code

Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

What is the forwardRef? Reactforwardref.js folds up the __DEV__ related code, and it’s just an incredibly simple higher-order component. Take a Render FunctionComponent, wrap it around it and define $$typeof as REACT_FORWARD_REF_TYPE, return.

Trace the code to find the resolveLazyComponentTag, where the $$Typeof is resolved to the corresponding WorkTag.

The WorkTag for REACT_FORWARD_REF_TYPE is ForwardRef. Then the ForwardRef enters the updateForwardRef logic.

case ForwardRef: {
  child = updateForwardRef(
    null,
    workInProgress,
    Component,
    resolvedProps,
    renderLanes,
  );
  return child;
}
Copy the code

This method in turn calls the renderWithHooks method, passing ref on the fifth argument.

nextChildren = renderWithHooks(
  current,
  workInProgress,
  render,
  nextProps,
  ref, / / here
  renderLanes,
);
Copy the code

Continuing to trace the code into the renderWithHooks method, you can see that ref is passed as the second parameter to Component. The FuncitonComponent wrapped by the forwardRef comes with the second argument ref (Context).

Knowing how to pass ref, the next question is how ref is assigned.

The assignment of ref

The break point is traced to the commitAttachRef, which checks whether the ref on the Fiber node is a function or a RefObject. Handle instance by type. If the Fiber node is the HostComponent (Tag = 5), which is the DOM node, instance is the DOM node. If the Fiber node is ClassComponent (tag = 1), instance is the object instance.

function commitAttachRef(finishedWork) {
  var ref = finishedWork.ref;

  if(ref ! = =null) {
    var instanceToUse = finishedWork.stateNode;

    if (typeof ref === 'function') {
      ref(instanceToUse);
    } else{ ref.current = instanceToUse; }}}Copy the code

This is the HostComponent and ClassComponent assignment logic for ref. For the ForwardRef component, it’s a different code, but the behavior is basically the same. See imperativeHandleEffect here.

Next, we’ll dig into the React source code to see how useRef is implemented.

Internal implementation of useRef

Locate the code ReactFiberHooks for the useRef runtime by tracing the code

There are two methods, mountRef and updateRef, which correspond to Fiber node mount and update operations on ref.

function updateRef<T> (initialValue: T) :{|current: T|} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

function mountRef<T> (initialValue: T) :{|current: T|} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}
Copy the code

As you can see from mount, useRef creates a RefObject and assigns it to Hook’s memoizedState, which is taken out and returned when update.

Different Hook memoizedState stores different contents, useState stores the state information, useEffect stores the effect object, useRef stores the ref object…

MountWorkInProgressHook, behind updateWorkInProgressHook method is a linked list of Hooks, in the case of not modify linked list, each render useRef can retrieve the same memoizedState object, It’s that simple 🍳.

Application: Merge ref

Now you have learned about the transfer and assignment logic of ref in React and the source code for useRef. We have an Input component that requires innerRef HTMLInputElement to access the DOM node from within the component, but we also allow the component to access the DOM node from the outside.

const Input = forwardRef((props, ref) = > {
  const innerRef = useRef<HTMLInputElement>(null)
  return (
    <input {. props} ref={????? } />)})Copy the code

Consider the?? How to write it.

============ answer line ==============

With an understanding of the RefCallback’s internal implementation, it is obvious that we can create a RefCallback that assigns values to multiple RefS.

export function combineRefs<T = any> (
  refs: Array<MutableRefObject<T | null> | RefCallback<T>>
) :React.RefCallback<T> {
  return value= > {
    refs.forEach(ref= > {
      if (typeof ref === 'function') {
        ref(value);
      } else if(ref ! = =null) { ref.current = value; }}); }; }const Input = forwardRef((props, ref) = > {
  const innerRef = useRef<HTMLInputElement>(null)
  return (
    <input {. props} ref={combineRefs(ref, innerRef)} / >)})Copy the code

The resources

  • React Technology revealed