Explanation#
-
This article will analyze the following two points
- How useRef stores data
- How Refs reference the real DOM
-
Before reading this article, please read React Hooks: hooks linked list
-
Version based on v18.1.0
-
Analysis based on the following code
import { useRef } from "react";
function UseRef() {
const ref1 = useRef(null);
const ref2 = useRef({
a: 1,
});
const handleClick = () => {
ref2.current = {
a: 3
}
}
return (
<div id="refTest" ref={ref1} onClick = {handleClick}>123</div>
)
}
export default UseRef;
TLNR#
- The data in useRef exists within its corresponding hook object (memoizedState), and modifying the data does not trigger a page update.
- The process of binding Refs to the DOM occurs within the layout phase of the commit stage.
useRef Analysis#
useRef in the mount scenario#
In the mount scenario, hitting the useRef will enter the mountRef function.
function mountRef<T>(initialValue: T): {|current: T|} {
// The content of the hooks linked list, used to create a hook object and mount it to the hooks linked list
const hook = mountWorkInProgressHook();
if (enableUseRefAccessWarning) {
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
} else {
// Create an object to store the initial value, which is the parameter passed in useRef
const ref = {current: initialValue};
// And store this object in the memoizedState property of the hook object
hook.memoizedState = ref;
return ref;
}
}
There is very little to analyze here.
useRef in the update scenario#
Like other hooks, it also exists in the update phase. Since the code is simple, it is directly pasted here.
function updateRef<T>(initialValue: T): {|current: T|} {
// updateWorkInProgressHook will create a new hook object based on the corresponding hook object on the current tree
// The reference address of ref (stored in memoizedState) will also be assigned to the new hook's memoizedState
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
The code is straightforward. Of course, we do not need to call dispatch for changes to the value inside useRef like we do with useState, so scheduleUpdateOnFiber will not be called, and this change will not trigger React's rendering.
Summary#
The value passed into useRef will be assigned to ref.current, and ref will be mounted to the memoizedState property of the corresponding hook object of useRef. Each time a new hook object is created in the update phase, the memoizedState from the old hook object will be assigned to the new hook object, ensuring that the reference address of ref remains unchanged, thus ensuring that the stored value remains unchanged (which means that the ref object returned on each render is the same object). Moreover, changes to the value stored in ref will not trigger React's rendering.
Refs#
Since the content of useRef is too little, I would like to discuss the ref in the example further.
However, the debugging thought process regarding refs is relatively complex, as JSX is transpiled by Babel into React.createElement. This function exists in packages/react/src/ReactElement.js
. The type declaration of the returned value is
{
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type: type,
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner,
};
It is clear that ref is separated out, but if debugging is needed, we must focus on the process of creating fiber based on ReactElement, which is within beginWork. However, this is divided into two phases — 1. UseRef's beginWork; 2. div#refTest's beginWork.
UseRef's beginWork#
When entering UseRef's beginWork, mountIndeterminateComponent will run.
/**
* Removed many unnecessary codes
*/
function mountIndeterminateComponent(...) {
...
/**
* renderWithHooks will return the result returned by UseRef
* that is, the ReactElement returned by this function component
* */
value = renderWithHooks(...)
...
/**
* reconcileChildren passes the ReactElement to diff and generate fiber
*/
reconcileChildren(null, workInProgress, value, renderLanes);
}
function renderWithHooks(...) {
...
let children = Component(props, secondArg);
...
return children;
}
The reconcileChildren function is a familiar face in interviews; it is the entry point for diffing. However, we will not discuss diffing here, just look at the part related to refs (reconcileChildren -> ChildReconciler -> reconcileSingleElement).
It is evident that after creating the new fiber node, the ref will be assigned to fiber.ref (the value returned by coerceRef is actually the ref on the ReactElement).
div#refTest's beginWork#
According to the breakpoint records, it will enter updateHostComponent, where the code related to ref exists in markRef.
function markRef(current: Fiber | null, workInProgress: Fiber) {
// Here, ref is ref1 in the example
const ref = workInProgress.ref;
/**
* When either of the following two conditions is met, the execution body will be entered
* 1. On the first update, ref is not null
* 2. On non-first updates, the ref on WIP is different from the ref on current, indicating a change
*/
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
// Schedule a Ref effect
/**
* Marks Ref on WIP's flags through bitwise operations to indicate an update has occurred
*/
workInProgress.flags |= Ref;
if (enableSuspenseLayoutEffectSemantics) {
workInProgress.flags |= RefStatic;
}
}
}
markRef, as its name suggests, primarily marks the fiber as having a ref.
Phase Summary 1#
The logic related to ref in beginWork actually does two things:
- After creating the fiber node, the ref property on the ReactElement is assigned to fiber.ref.
- If the ref on the currently traversed fiber node is not null or its value has changed, it marks this fiber node (workInProgress.flags |= Ref;).
How refs bind to the DOM#
Clearly, the logic for refs is not finished here; we have completed the initialization of refs, but what about the assignment? How do we bind the corresponding DOM to refs?
To explain this issue, we need to refer to the official description of refs:
React will pass the DOM element to the current property when the component mounts, and pass null when the component unmounts. The ref will update before the componentDidMount or componentDidUpdate lifecycle hooks are triggered.
Since the description is related to lifecycle, we can only write a class example to see.
import React from "react";
class UseRef extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
componentDidMount() {
console.log(this.myRef);
}
render() {
return <div ref={this.myRef} />;
}
}
export default UseRef;
By hitting a breakpoint in componentDidMount, we can see the following call stack:
It is clear that the call to componentDidMount occurs in the commit phase (the entry point of the commit phase is commitRootImpl), and the commit phase is further divided into three sub-phases:
- beforeMutation, entry point is commitBeforeMutationEffects
- mutation, entry point is commitMutationEffects
- layout, entry point is commitLayoutEffects
In these three phases, only in the layout phase can we access the fully processed DOM structure. Therefore, the steps for binding refs to the DOM are also suitable to be handled here.
In fact, this is indeed the case.
The assignment for refs is hidden in the commitLayoutEffects' commitLayoutEffectOnFiber.
function commitLayoutEffectOnFiber(...) {
...
if (!enableSuspenseLayoutEffectSemantics || !offscreenSubtreeWasHidden) {
if (enableScopeAPI) {
// TODO: This is a temporary solution that allowed us to transition away
// from React Flare on www.
/**
* finishedWork.flags & Ref
* & operator can indicate whether it contains a feature; if it does not contain the feature, the result is 0
* For example, 0110 indicates all features owned, 0010 indicates a specific feature
* To check if 0110 has feature 0010, run 0110 & 0010 to get the result 0010
* If the result is non-zero, it can be determined that 0110 contains 0010
*/
if (finishedWork.flags & Ref && finishedWork.tag !== ScopeComponent) {
commitAttachRef(finishedWork);
}
} else {
if (finishedWork.flags & Ref) {
commitAttachRef(finishedWork);
}
}
}
...
}
Do you remember the mark we placed on the fiber during beginWork? This function uses it. When the flags contain the Ref mark, commitAttachRef is executed.
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref; // ref on fiber, at this point ref.current is null
if (ref !== null) {
const instance = finishedWork.stateNode; // stateNode holds the DOM object
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
...
if (typeof ref === 'function') {
let retVal;
...
retVal = ref(instanceToUse); // refs can also be callbacks...
} else {
ref.current = instanceToUse; // This is the logic for binding the DOM, after assignment it ensures that refs' DOM is up to date.
}
}
}
Phase Summary 2#
The binding of refs to the DOM occurs in the layout phase of the commit.