
How to quickly get started with VUE3.0: Enter and learn
In React projects, there are many scenarios where Ref is needed. For example, use ref attribute to obtain the DOM node and obtain the ClassComponent object instance; use useRef Hook to create a Ref object to solve the problem of setInterval not being able to obtain the latest state; you can also call the React.createRef method to manually create a Ref object.
Although Ref is very simple to use, it is still inevitable to encounter problems in actual projects. This article will sort out various Ref -related issues from the source code perspective and clarify what is done behind ref related APIs. . After reading this article, you may have a deeper understanding of Ref .
First of all, ref is the abbreviation of reference , which is a reference. In the react type declaration file, you can find several Ref-related types, and they are listed here.
interface RefObject<T> { readonly current: T | null; }
interface MutableRefObject<T> { current: T; } When using useRef Hook, RefObject/MutableRefObejct is returned. Both types define an object structure of { current: T } . The difference is that the current property of RefObject is read-only. Yes, Typescript will warn ⚠️ if refObject.current is modified.
const ref = useRef<string>(null) ref.current = '' // Error
TS: Cannot be assigned to "current" because it is a read-only property.

Look at the definition of the useRef method. Function overloading is used here. When the incoming generic parameter T does not contain null , RefObject<T> is returned. When it contains null , MutableRefObject<T> is returned.
function useRef<T>(initialValue: T): MutableRefObject<T>; function useRef<T>(initialValue: T | null): RefObject<T>;
So if you want the current property of the created ref object to be modifiable, you need to add | null .
const ref = useRef<string | null>(null) ref.current = '' // OK,
when calling the React.createRef() method, a RefObject is also returned.
createRef
export function createRef(): RefObject {
const refObject = {
current: null,
};
if (__DEV__) {
Object.seal(refObject);
}
return refObject;
} RefObject/MutableRefObject was added in version 16.3 . If you use earlier versions, you need to use Ref Callback .
Using Ref Callback is to pass a callback function. When react calls back, the corresponding instance will be passed back, and it can be saved by itself for calling. The type of this callback function is RefCallback .
type RefCallback<T> = (instance: T | null) => void;
Example of using RefCallback :
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} />
);
}
} In the type declaration, there are also Ref/LegacyRef types, which are used to refer to the Ref type generally. LegacyRef is a compatible version. In the previous old version, ref could also be a string.
type Ref<T> = RefCallback<T> | RefObject<T> | null; type LegacyRef<T> = string | Ref<T>;
Only by understanding the types related to Ref can you become more comfortable writing Typescript.
PassingWhen using ref on a JSX component, we set a Ref to the ref attribute. We all know that the syntax of jsx will be compiled into the form of createElement by tools such as Babel.
//jsx
<App ref={ref} id="my-app" ></App>
// compiled to
React.createElement(App, {
ref: ref,
id: "my-app"
}); It seems that ref is no different from other props, but if you try to print props.ref inside the component, it is undefined . And the dev environment console will give prompts.
Trying to access it will result in
undefinedbeing 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 with ref? As you can see in the ReactElement source code, ref is RESERVED_PROPS . key also have this treatment. They will be specially processed and extracted from props and passed to Element .
const RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true,
}; So ref is “props“ that will be treated specially.
Before version 16.8.0 , Function Component was stateless and would only render based on the incoming props. With Hook, you can not only have internal state, but also expose methods for external calls (requiring forwardRef and useImperativeHandle ).
If you use ref directly for a Function Component , the console in the dev environment will warn you that you need to wrap it with forwardRef .
functionInput () {
return <input />
}
const ref = useRef()
<Input ref={ref} /> Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
What is forwardRef ? View the source code ReactForwardRef.js. Fold the __DEV__ related code. It is just an extremely simple high-order component. Receive a rendered FunctionComponent, wrap it and define $$typeof as REACT_FORWARD_REF_TYPE , and return it.

Trace the code and find resolveLazyComponentTag, where $$typeof will be parsed into the corresponding WorkTag.

The WorkTag corresponding to REACT_FORWARD_REF_TYPE is ForwardRef. Then ForwardRef will enter the logic of updateForwardRef.
case ForwardRef: {
child = updateForwardRef(
null,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
return child;
} This method will call the renderWithHooks method and pass in ref in the fifth parameter.
nextChildren = renderWithHooks( current, workInProgress, render, nextProps, ref, // here renderLanes, );
Continue to trace the code and enter the renderWithHooks method. You can see that ref will be passed as the second parameter of Component . At this point we can understand where the second parameter ref of FuncitonComponent wrapped by forwardRef comes from (compared to the second parameter of ClassComponent contructor which is Context).

Knowing how to pass ref, the next question is how ref is assigned.
The(assign a RefCallback to ref and break the point in the callback). Trace the code commitAttachRef. In this method, it will be judged whether the ref of the Fiber node is function or RefObject, and the instance will be processed according to the type. If the Fiber node is a HostComponent ( tag = 5 ), which is a DOM node, instance is the DOM node; and if the Fiber node is a 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;
}
}
} The above is the assignment logic of ref in HostComponent and ClassComponent. For ForwardRef type components, different codes are used, but the behavior is basically the same. You can see the imperativeHandleEffect here.
Next, we continue to dig into the React source code to see how useRef is implemented.
locates the useRef runtime code ReactFiberHooks by tracking the code

There are two methods here, mountRef and updateRef . As the name suggests, they correspond to the operations on ref when Fiber node mount and update .
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;
} You can see that when mount , useRef creates a RefObject and assigns it to hook 's memoizedState . When update , it is taken out and returned directly.
Different Hook memoizedState saves different contents. useState saves state information, useEffect saves effect objects, useRef saves ref objects...
mountWorkInProgressHook and updateWorkInProgressHook methods are backed by a linked list of Hooks. When the linked list is not modified, Next, you can retrieve the same memoizedState object every time you render useRef. It's that simple.
At this point, we understand the logic of passing and assigning ref in React, as well as the source code related to useRef . Use an application question to consolidate the above knowledge points: There is an Input component. Inside the component, innerRef HTMLInputElement needs to be used to access the DOM node. At the same time, it also allows the external component to ref the node. How to implement it?
const Input = forwardRef((props, ref) => {
const innerRef = useRef<HTMLInputElement>(null)
return (
<input {...props} ref={???} />
)
}) Consider how ??? in the above code should be written.
============ Answer dividing line ==============
By understanding the internal implementation related to Ref, it is obvious that we can create a RefCallback here, which can handle multiple Just assign a ref .
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)} />
)
})