preface

If you’ve used React before 16, your first impression is to access the DOM or modify component instances, as described on the website:

In React 16.3 (createRef) and 16.8 hooks (useRef), it seems that the refs are not only bound to DOM/ component instances. This article will try to analyze the relevant source code, take you thoroughly understand React REF.

Front knowledge

In order to facilitate the understanding of this article, a few knowledge points are briefly mentioned here.

The Fiber architecture

Fiber is the smallest unit when React is updated. It is a data structure containing Pointers. In terms of data structure, Fiber architecture is ≈ tree + linked list. Fiber units are generated from the JSX createElement based on the ReactElement. Compared to the ReactElement, Fiber units have the ability to work dynamically.

React workflow

Record a React application rendering using Chrome Perfomance and look at the function call stack to see the following diagram

These three pieces represent:

  1. Generate the React root node
  2. The Reconciler coordinates the generation of child nodes that need to be updated
  3. Update the node to the commit view (commit phase)

Hooks basic knowledge

Each time a hook function starting with use is executed in a function component, a hook object is generated.

type Hook = {
  memoizedState: any.// The final state value since the last update
  queue: UpdateQueue, // Update the queue
  next, // The next hook object
};
Copy the code

MemoizedState will save the final state of the hook after the last update. For example, when we use useState once, we will save the initial value in memoizedState.

Most hooks in React are divided into two phases: the mount phase during the first initialization and the update phase

The hooks function executes in two stages: mount and update. For example, useState only executes once during initialization. Examples include useImperativeHandle and useRef, which are mentioned below.

Debugging the source code

This article has combed through the functions associated with the source code, but it’s much more effective if you eat them together with source code debugging.

This paper is based on React V17.0.2.

  1. Pull the React code and install the dependencies
  2. Package React, Scheduler, and React-DOM as CommonJS
yarn build react/index,react-dom/index,scheduler --type NODE
Copy the code
  1. Go to build/node_modules/react/ CJS and run yarn link as react-dom
  2. In the build/node_modules/react/CJS/react. Development. The js to put a marker on the link in the console to ensure that the check state of the link
  3. Create a test application using create-react-app and link react, react-dom

Let’s go ahead and open the body

ref prop

The ref property on a component is a reserved property. You cannot retrieve the ref property in a component as if it were a normal prop property. For example:

const Parent = () = > {
	return <Child ref={{test:1}}>} const Child = (props) => { console.log(props); // The return of the ref attribute is not obtained<div></div>
}
Copy the code

Where does this ref go, and what does React itself do to it?

We know that React parsing starts with createElement. We found the following place to create the ReactElement. There is indeed processing of the ref-reserved attribute.

export function createElement(type, config, children) {
let propName;
  // Reserved names are extracted
  const props = {};
  let ref = null;
  if(config ! =null) {
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    for (propName in config) {
      if( hasOwnProperty.call(config, propName) && ! RESERVED_PROPS.hasOwnProperty(propName) ) { props[propName] = config[propName]; }}}returnReactElement( type, key, ref, props, ... ) ; }Copy the code

A reference to the REF attribute has been created since createElement.

After createElement we need to build the Fiber working tree. Next we will focus on the ref related processing.

React handles different components differently

To focus on the first FunctionComponent ClassComponent/HostComponent (native HTML tags)

FunctionComponent

function updateFunctionComponent(current, workInProgress, Component, nextProps, renderLanes) {
      try {
        nextChildren = renderWithHooks(current, workInProgress, Component, nextProps, context, renderLanes);
      } finally {
        reenableLogs();
      }
      reconcileChildren(current, workInProgress, nextChildren, renderLanes);
      return workInProgress.child;
}
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes){
  			children = Component(props, secondArg); // Component refers to our function Component
				return children;
}
Copy the code

We can see that the function component is executed directly at rendering time.

Ref Prop for Class components and native labels

ClassComponent

function updateClassComponent(current, workInProgress, Component, nextProps, renderLanes) {... {... constructClassInstance(workInProgress, Component, nextProps); . }varnextUnitOfWork = finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes); .return nextUnitOfWork;
}

function constructClassInstance(workInProgress, ctor, props) {...var instance = new ctor(props, context);
  // Mount the instance to the workInProgress stateNode propertyadoptClassInstance(workInProgress, instance); .return instance;
}
function finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes) {
  // Mark whether the ref has been updated
  markRef(current, workInProgress);
}

function markRef(current, workInProgress) {
  var ref = workInProgress.ref;

  if (current === null&& ref ! = =null|| current ! = =null&& current.ref ! == ref) {// Schedule a Ref effectworkInProgress.flags |= Ref; }}Copy the code

ClassComponent uses the constructor to generate an instance and mark the REF property.

As a review of the React workflow mentioned earlier, if you are assigning component instances or real DOM values to the ref, you should not process the REF at the beginning, but assign values to the REF according to the mark until the commit phase.

function commitLayoutEffectOnFiber(finishedRoot, current, finishedWork, committedLanes) {... {if(finishedWork.flags & Ref) { commitAttachRef(finishedWork); }}... }function commitAttachRef(finishedWork) {
  var ref = finishedWork.ref;
  if(ref ! = =null) {
    var instance = finishedWork.stateNode;
    var instanceToUse;
    switch (finishedWork.tag) {
      case HostComponent:
        // getPublicInstance calls the DOM API and returns the DOM object
        instanceToUse = getPublicInstance(instance);
        break;

      default:
        instanceToUse = instance;
    } 
    // Set ref processing for function callback form
    if (typeof ref === 'function') { { ref(instanceToUse); }}else{ ref.current = instanceToUse; }}}Copy the code

In the commit phase, the real DOM is assigned to the current property of the REF object if it is a native tag, or to the component instance if it is class Componnet.

Function component’s ref Prop

If you add ref to the function component without processing it, React will ignore it and alert the development environment

What if the function component has no instance to assign to the ref object, and the ref Prop on the component is treated as a reserved property and cannot be obtained in the component?

forwardRef

React provides a forwardRef function to handle the ref prop of its component, as shown in this example:

const Parent = () = > {
	const childRef = useRef(null)
  return <Child ref={childRef}/>
}

const Child = forWardRef((props,ref) = > {
	return <div>Child</div>
}}
Copy the code

The source body of this method is also very simple, returning a new elementType object whose render property contains the original function component, and the $$Typeof marks the special component type.

function forwardRef(render) {...var elementType = {
    $$typeof: REACT_FORWARD_REF_TYPE,
    render: render
  }
  ....
  return elementType;
 }
Copy the code

How does React handle the forwardRef special component

function beginWork(current, workInProgress, renderLanes) {...switch (workInProgress.tag) {
    case FunctionComponent:
      {
       ...
        return updateFunctionComponent(current, workInProgress, _Component, resolvedProps, renderLanes);
      }

    case ClassComponent:
      {
				....
        return updateClassComponent(current, workInProgress, _Component2, _resolvedProps, renderLanes);
      }

    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    case ForwardRef:
      {
				....
        // The third parameter is the elementType created by the forwardRef
        returnupdateForwardRef(current, workInProgress, type, _resolvedProps2, renderLanes); }}function updateForwardRef(current, workInProgress, Component, nextProps, renderLanes) {...var render = Component.render;
  var ref = workInProgress.ref; // The rest is a fork of updateFunctionComponent

  varnextChildren; {...// Pass the ref reference to renderWithHooksnextChildren = renderWithHooks(current, workInProgress, render, nextProps, ref, renderLanes); . } workInProgress.flags |= PerformedWork; reconcileChildren(current, workInProgress, nextChildren, renderLanes);return workInProgress.child;
}
Copy the code

As you can see, the main difference with FunctionComponent above is that it passes the renderWithHooks method with the REF reserve attribute as a normal attribute!

If you pass a ref reference without attaching an instance like the Class component, is there no way to manipulate the behavior of the sub-function component?

Use the example above to verify

const Parent = () = > {	
  const childRef = useRef(null)
  useEffect(() = >{
  	console.log(childref) // { current:null }
  })
  return <Child ref={childRef}/>
}

const Child = forwardRef((props,ref) = > {
	return <div>Child</div>
}}
                         
 const Parent = () = > {	
  const childRef = useRef(null)
  useEffect(() = >{
  	console.log(childref) // { current: div }
  })
  return <Child ref={childRef}/>
}

const Child = forwardRef((props,ref) = > {
	return <div ref={ref}>Child</div>
}}
Copy the code

ForwardRef can only forward the ref attribute if the forwardRef is used alone. If the ref does not end up binding to a ClassCompnent or native DOM then the ref will not change.

Imagine a business scenario where you encapsulate a form component and want to expose some of the interfaces such as submitting action and validation. What should you do?

useImperativeHandle

React gives us this hook to help the function component expose the properties to the outside so let’s see what happens

const Parent = () = > {	
  const childRef = useRef(null)
  useEffect(() = >{
  	chilRef.current.sayName();// child
  })
  return <Child ref={childRef}/>
}

const Child = forwardRef((props,ref) = > {
  useImperativeHandle(ref,() = >({
  	sayName:() = >{
    	console.log('child')}}))return <div>Child</div>
}}
Copy the code

Take a look at the source section of the hook (take the hook mount phase as an example) :

useImperativeHandle: function (ref, create, deps) {
      currentHookNameInDev = 'useImperativeHandle';
      mountHookTypesDev();
      checkDepsAreArrayDev(deps);
      return mountImperativeHandle(ref, create, deps);
 }

function mountImperativeHandle(ref, create, deps) {{if (typeofcreate ! = ='function') {
      error('Expected useImperativeHandle() second argument to be a function ' + 'that creates a handle. Instead received: %s.', create ! = =null ? typeof create : 'null'); }}// TODO: If deps are provided, should we skip comparing the ref itself?


  vareffectDeps = deps ! = =null&& deps ! = =undefined ? deps.concat([ref]) : null;
  var fiberFlags = Update;

  return mountEffectImpl(fiberFlags, Layout, imperativeHandleEffect.bind(null, create, ref), effectDeps);
}

function imperativeHandleEffect(create, ref) {
  if (typeof ref === 'function') {
    var refCallback = ref;

    var _inst = create();

    refCallback(_inst);
    return function () {
      refCallback(null);
    };
  } else if(ref ! = =null&& ref ! = =undefined) {
    var refObject = ref;

    {
      if(! refObject.hasOwnProperty('current')) {
        error('Expected useImperativeHandle() first argument to either be a ' + 'ref callback or React.createRef() object. Instead received: %s.'.'an object with keys {' + Object.keys(refObject).join(', ') + '} '); }}// The second argument passed to hook is executed
    var _inst2 = create();

    refObject.current = _inst2;
    return function () {
      refObject.current = null; }; }}Copy the code

We assign to the current object of ref the object we need to expose and the result of the second function parameter passed to useImperativeHandle.

Same quote

So far we’ve outlined the workflow of ref Prop on components, and how to use ref Prop in functional components. It’s easier than expected.

Notice that the ref seems to be passed from createElement to WorkInProgess Fiber tree to commit.

The code for the intermediate process is too large and complex, but we can verify it with a simple test.

const isEqualRefDemo = () = > {
	const isEqualRef = useRef(1)
  return <input key="test" ref={isEqualRef}>
}
Copy the code

CreateElement before commitAttachRef for class Component and native tags:

Mount the ref to the window object in the createElement and check whether the two refs are identical in the commitAttachRef.

For a function component this is before creating an Element into the hook to execute the imperativeHandleEffect:

const Parent = () = > {	
  const childRef = useRef(1)
  useEffect(() = >{
  	chilRef.current.sayName();// child
  })
  return <Child ref={childRef}/>
}

const Child = forwardRef((props,ref) = > {
  useImperativeHandle(ref,() = >({
  	sayName:() = >{
    	console.log('child')}}))return <div>Child</div>
}}
Copy the code

The ref is the same reference from the time the REF is added to createElement until it is assigned at the end of the React rendering process (the COMMIT phase). This is just like the original ref reference.

Section summarizes

  1. Ref is a reserved property when it appears on a component
  2. The ref maintains the same reference (MutableObject) for the lifetime of the component.
  3. If the object that ref mounts is a native HTML tag the current property of the ref object will be assigned to the real DOM and if it’s a React component it will be assigned to React” component instance”
  4. Ref mounts are all processed in the COMMIT phase

How to create the REF

The ref prop is equivalent to digging a “hole” in the component to undertake the REF object, but this is not enough we need to create the REF object first

The string ref & callback ref

These two ways to create the REF will not be repeated, the website and the community excellent articles for reference.

  • Zh-hans.reactjs.org/docs/refs-a…
  • Blog.logrocket.com/how-to-use-…

createRef & useRef

createRef

The createRef API was introduced in 16.3

The source code for createRef is a closure that exposes an object with the current property.

This is how we usually use createRef in a class Component

class CreateRefComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef()
  }
  componentDidMount() {
    this.myRef.current.focus()
  	console.log(this.myRef.current)
    // dom input
  }
  render() {
    return <input ref={this.myRef} />}}Copy the code

Why can’t YOU use createRef in a function component

Combining the first section with the source code for createRef, we see that this is nothing more than a mutable object mounted inside a class component. Because class component constructors are not executed repeatedly, the createRef naturally remains the same reference. However, when it comes to function components, createRef will be recreated and executed repeatedly every time the component is updated, so using createRef in function components will not achieve the effect of only having the same reference.

const CreateRefInFC = () = > {
  const valRef = React.createRef();  // If createRef is used in the function component, in this example the ref will be recreated and therefore will always be null
  const [, update] = React.useState();
  return <div>
    value: {valRef.current}
    <button onClick={()= >{ valRef.current = 80; update({}); > +}}</button>
  </div>
}
Copy the code

useRef

There were hooks in React 16.8, which allowed us to define state in function components, as well as useRef

Let’s take a look at what moutRef and updateRef did:

function mountRef(initialValue) {
  var hook = mountWorkInProgressHook();

  {
    var _ref2 = {
      current: initialValue
    };
    hook.memoizedState = _ref2;
    return_ref2; }}function updateRef(initialValue) {
  var hook = updateWorkInProgressHook();
  return hook.memoizedState;
}
Copy the code

With the hook data structure, the value created on the first useRef is saved in memoizedState and returned directly after each update phase.

In this way, useRef will return the same reference when the function component is updated.

So we essentially create a Mutable Object just like createRef, just because the rendering method is different, we do some processing in the function component. The mounting and unmounting activities are all maintained by the component itself.

The extended ref

Starting with createRef, we can see that the consumption of ref objects is no longer tied to the DOM and component properties, which means you can consume them anywhere, which answers the question at the beginning of this article.

The application of useRef

Solve the closure problem

Because of the closure formed each time the function component executes, the following code always prints 1

export const ClosureDemo =  () = > {
    const [ count,setCount ] = useState(0);
    useEffect(() = > {
        const interval = setInterval(() = >{
          setCount(count+1)},1000)
        return () = > clearInterval(interval)
      }, [])
    // Count is always 1
    return <div>{ count }</div>
}
Copy the code

Pass count as a dependency to useEffect to solve this problem

export const ClosureDemo =  () = > {
    const [ count,setCount ] = useState(0);
    useEffect(() = > {
        const interval = setInterval(() = >{
          setCount(count+1)},1000)
        return () = > clearInterval(interval)
      }, [count])
    return <div>{ count }</div>
}
Copy the code

But the timer is also constantly created as the count value is updated, which causes performance problems (less obvious in this example) and, more importantly, does not fit our development semantics, since we obviously want the timer itself to be constant.

There is another way to deal with this problem

export const ClosureDemo =  () = > {
    const [ count,setCount ] = useState(0);
    useEffect(() = > {
        const interval = setInterval(() = >{
          setCount(count= > count + 1) // Use the setSate function to ensure that the new value is retrieved each time
        }, 1000)
        return () = > clearInterval(interval)
      }, [])
    return <div>{ count }</div>
}
Copy the code

This does handle the impact of closures, but only in cases where you need to use setState; changes to data and triggering setState require binding, which may cause unnecessary refreshes.

Use useRef to create the reference

export const ClosureDemo =  () = > {
    const [ count,setCount ] = useState(0);
  	const countRef = useRef(0);
  	countRef.current = count
    useEffect(() = > {
        const interval = setInterval(() = >{
          // This decouples the logic that updates the count from the logic that triggers the update
          if(countRef.current < 5){
          	countRef.current++
          } else {
          	setCount(countRef.current)
          }
        }, 1000)
        return () = > clearInterval(interval)
      }, [])
    return <div>{ count }</div>
}
Copy the code

Encapsulates custom hooks

useCreation

Use the Factory function to avoid repeated execution of constructors like the one in useRef(new Construcotr)

import { useRef } from 'react';

export default function useCreation<T> (factory: () => T, deps: any[]) {
  const { current } = useRef({
    deps,
    obj: undefined as undefined | T,
    initialized: false});if (current.initialized === false| |! depsAreSame(current.deps, deps)) { current.deps = deps; current.obj = factory(); current.initialized =true;
  }
  return current.obj as T;
}

function depsAreSame(oldDeps: any[], deps: any[]) :boolean {
  if (oldDeps === deps) return true;
  for (const i in oldDeps) {
    if(oldDeps[i] ! == deps[i])return false;
  }
  return true;
}
Copy the code
usePrevious

Save the previous state by creating two refs

import { useRef } from 'react';

export type compareFunction<T> = (prev: T | undefined, next: T) = > boolean;

function usePrevious<T> (state: T, compare? : compareFunction
       ) :T | undefined {
  const prevRef = useRef<T>();
  const curRef = useRef<T>();

  const needUpdate = typeof compare === 'function' ? compare(curRef.current, state) : true;
  if (needUpdate) {
    prevRef.current = curRef.current;
    curRef.current = state;
  }

  return prevRef.current;
}

export default usePrevious;
Copy the code
useClickAway

Custom element out of focus response hook

import { useEffect, useRef } from 'react';

export type BasicTarget<T = HTMLElement> =
  | (() = > T | null)
  | T
  | null
  | MutableRefObject<T | null | undefined>;
  
 export function getTargetElement(
  target?: BasicTarget<TargetElement>,
  defaultElement?: TargetElement,
) :TargetElement | undefined | null {
  if(! target) {return defaultElement;
  }

  let targetElement: TargetElement | undefined | null;

  if (typeof target === 'function') {
    targetElement = target();
  } else if ('current' in target) {
    targetElement = target.current;
  } else {
    targetElement = target;
  }
  return targetElement;
}
// Click events do not listen to the right click
const defaultEvent = 'click';

type EventType = MouseEvent | TouchEvent;

export default function useClickAway(
  onClickAway: (event: EventType) => void,
  target: BasicTarget | BasicTarget[],
  eventName: string = defaultEvent,
) {
  // Use useRef to save the callback function
  const onClickAwayRef = useRef(onClickAway);
  onClickAwayRef.current = onClickAway;

  useEffect(() = > {
    const handler = (event: any) = > {
      const targets = Array.isArray(target) ? target : [target];
      if (
        targets.some((targetItem) = > {
          const targetElement = getTargetElement(targetItem) as HTMLElement;
          return! targetElement || targetElement? .contains(event.target); {}))return;
      }
      onClickAwayRef.current(event);
    };

    document.addEventListener(eventName, handler);

    return () = > {
      document.removeEventListener(eventName, handler);
    };
  }, [target, eventName]);
}

Copy the code

The above custom hooks are from Ahooks

There are also many useful custom hooks and repositories, such as React-use, which have many custom hooks based on useRef.

The resources

  • The React Fiber juejin. Cn/post / 684490…
  • The React website USES zh-hans.reactjs.org/docs/refs-a ref…
  • This life the React former zhuanlan.zhihu.com/p/40462264
  • React ref source analysis blog.csdn.net/qq_32281471…

The last

Search the official wechat account Eval Studio for more updates.