How does useRef() work?
useRef()
quite a lot, let’s figure out its internals in React.
1. intro of useRef()
We create a ref by useRef()
, then programmatically set its .current
property, or use it as ref
prop on DOM elements.
jsx
function Component() {const ref = useRef(null);return <div ref={ref} />;}
jsx
function Component() {const ref = useRef(null);return <div ref={ref} />;}
So let’s try to solve 2 puzzles here.
- how
useRef()
works in initial render and rerender? - how
ref={ref}
works?
2. how useRef()
works?
As covered before, let’s search directly to mountRef()
and updateRef()
which is used in initial render and rerender, and yeah bingo.
2.1. mountRef()
js
function mountRef<T>(initialValue: T): {| current: T |} {const hook = mountWorkInProgressHook();const ref = { current: initialValue };hook.memoizedState = ref;return ref;}
js
function mountRef<T>(initialValue: T): {| current: T |} {const hook = mountWorkInProgressHook();const ref = { current: initialValue };hook.memoizedState = ref;return ref;}
It is super simple
- create a ref objec which has
current
property - create a new hook by
mountWorkInProgressHook()
- set ref to
memoizedState
for the ref.
We can ignore for the naming of memoizedState
, it is just some internal naming for hooks to hold the state stuff.
mountRef()
is pretty straightforward, I guess for updateRef()
, we’d just return the ref
object again? Let’s move on
2.2. updateRef()
js
function updateRef<T>(initialValue: T): {| current: T |} {const hook = updateWorkInProgressHook();return hook.memoizedState;}
js
function updateRef<T>(initialValue: T): {| current: T |} {const hook = updateWorkInProgressHook();return hook.memoizedState;}
Yep, so simple.
updateWorkInProgressHook()
as mentioned in my previous videos, it is just to go through the hook list for the fiber, with a internal cursor. For initial render, the list is empty so it creates a new hook every time. For rerender, there is already a hook there, so it just use that hook.
3. How ref={ref}
works?
useRef()
is so simple, programmatically setting current
is just change the value of that property, so different from useState()
it doesn’t trigger update.
More interesting question is how it set with DOM element, which includes 2 sub questions.
- how
ref
is attached - how
ref
is detached
3.1 how ref
is attached
For details about how I figured it out, you can refer to my video, here I just listed up the important parts.
During commit phase, commitAttachRef()
is called inside of commitLayoutEffectOnFiber()
.
js
function commitAttachRef(finishedWork: Fiber) {const ref = finishedWork.ref;if (ref !== null) {const instance = finishedWork.stateNode;let instanceToUse;switch (finishedWork.tag) {case HostComponent:instanceToUse = getPublicInstance(instance);break;default:instanceToUse = instance;}// Moved outside to ensure DCE works with this flagif (enableScopeAPI && finishedWork.tag === ScopeComponent) {instanceToUse = instance;}if (typeof ref === "function") {let retVal;retVal = ref(instanceToUse);} else {ref.current = instanceToUse;}}}
js
function commitAttachRef(finishedWork: Fiber) {const ref = finishedWork.ref;if (ref !== null) {const instance = finishedWork.stateNode;let instanceToUse;switch (finishedWork.tag) {case HostComponent:instanceToUse = getPublicInstance(instance);break;default:instanceToUse = instance;}// Moved outside to ensure DCE works with this flagif (enableScopeAPI && finishedWork.tag === ScopeComponent) {instanceToUse = instance;}if (typeof ref === "function") {let retVal;retVal = ref(instanceToUse);} else {ref.current = instanceToUse;}}}
We can see that for DOM element, which is HostComponent, the DOM node is set here. Also it accept callback ref or ref object.
This means that the attaching of ref happens the same phase as layout effect, if callback ref is used here, then the attaching could be sooner than useEffect
, this could helps us understand React advanced patterns - Reusable behavior hooks through Ref.
3.2 how ref
is detached
Below this function there is the detaching function
js
function commitDetachRef(current: Fiber) {const currentRef = current.ref;if (currentRef !== null) {if (typeof currentRef === "function") {currentRef(null);} else {currentRef.current = null;}}}
js
function commitDetachRef(current: Fiber) {const currentRef = current.ref;if (currentRef !== null) {if (typeof currentRef === "function") {currentRef(null);} else {currentRef.current = null;}}}
We can see it also support callback ref or ref object. Search for commitDetachRef()
we can see that it happens in commitMutationEffectsOnFiber()
, which is even sooner than commitLayoutEffectOnFiber()
, which is reasonable, because we need to detach first to keep consistency.
3.3. React knows if needed to attach or detach by flags
There is some checks before above functions are called.
js
if (finishedWork.flags & Ref) {commitAttachRef(finishedWork);}if (flags & Ref) {const current = finishedWork.alternate;if (current !== null) {commitDetachRef(current);}}
js
if (finishedWork.flags & Ref) {commitAttachRef(finishedWork);}if (flags & Ref) {const current = finishedWork.alternate;if (current !== null) {commitDetachRef(current);}}
They use the same flag Ref
, if Ref
flag is set it means ref has changed, for the detaching, it checks current
, if it is not null then detach the ref on the older fiber, because the fiber is no longer used.
When is Ref
set? It is done in markRef()
(source)
js
function markRef(current: Fiber | null, workInProgress: Fiber) {const ref = workInProgress.ref;if ((current === null && ref !== null) ||(current !== null && current.ref !== ref)) {// Schedule a Ref effectworkInProgress.flags |= Ref;if (enableSuspenseLayoutEffectSemantics) {workInProgress.flags |= RefStatic;}}}
js
function markRef(current: Fiber | null, workInProgress: Fiber) {const ref = workInProgress.ref;if ((current === null && ref !== null) ||(current !== null && current.ref !== ref)) {// Schedule a Ref effectworkInProgress.flags |= Ref;if (enableSuspenseLayoutEffectSemantics) {workInProgress.flags |= RefStatic;}}}
We can see that it checks for ref creation and ref change.
markRef()
is called in updateHostComponent()
which is inside of reconciliation. (source)
function updateHostComponent(current: Fiber | null,workInProgress: Fiber,renderLanes: Lanes) {pushHostContext(workInProgress);if (current === null) {tryToClaimNextHydratableInstance(workInProgress);}const type = workInProgress.type;const nextProps = workInProgress.pendingProps;const prevProps = current !== null ? current.memoizedProps : null;let nextChildren = nextProps.children;const isDirectTextChild = shouldSetTextContent(type, nextProps);if (isDirectTextChild) {// We special case a direct text child of a host node. This is a common// case. We won't handle it as a reified child. We will instead handle// this in the host environment that also has access to this prop. That// avoids allocating another HostText fiber and traversing it.nextChildren = null;} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {// If we're switching from a direct text child to a normal child, or to// empty, we need to schedule the text content to be reset.workInProgress.flags |= ContentReset;}markRef(current, workInProgress);reconcileChildren(current, workInProgress, nextChildren, renderLanes);return workInProgress.child;}
function updateHostComponent(current: Fiber | null,workInProgress: Fiber,renderLanes: Lanes) {pushHostContext(workInProgress);if (current === null) {tryToClaimNextHydratableInstance(workInProgress);}const type = workInProgress.type;const nextProps = workInProgress.pendingProps;const prevProps = current !== null ? current.memoizedProps : null;let nextChildren = nextProps.children;const isDirectTextChild = shouldSetTextContent(type, nextProps);if (isDirectTextChild) {// We special case a direct text child of a host node. This is a common// case. We won't handle it as a reified child. We will instead handle// this in the host environment that also has access to this prop. That// avoids allocating another HostText fiber and traversing it.nextChildren = null;} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {// If we're switching from a direct text child to a normal child, or to// empty, we need to schedule the text content to be reset.workInProgress.flags |= ContentReset;}markRef(current, workInProgress);reconcileChildren(current, workInProgress, nextChildren, renderLanes);return workInProgress.child;}
Cool, we now know what is going on for refs.
4. Summary
- during reconciliation, ref changes/creation will be marked on fiber in
flags
- during committing, react will detach/attach the ref by checking
flags
useRef()
is a simple hook which just holds the ref object.
Want to know more about how React works internally?
Check out my series - React Internals Deep Dive!