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
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 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)} /> ) })