banner
IWSR

IWSR

我永远喜欢志喜屋梦子!

React.memo

TLNR#

  1. React.memo adds the REACT_MEMO_TYPE tag to the incoming component, which allows it to perform a shallow comparison of the incoming props (or compare the new and old props using a compare function) before each update to determine whether to reuse the original fiber component.
  2. If there is an update within the current component, the memo component will skip the comparison and directly generate a new fiber.

Explanation#

The analysis is based on the following code

import React, { useState, memo } from 'react';

const isEqual = (prevProps, nextProps) => {
  if (prevProps.number !== nextProps.number) {
      return false;
  }
  return true;
}

const ChildMemo = memo((props = {}) => {
  console.log(`--- memo re-render ---`);
  return (
      <div>
          <p>number is : {props.number}</p>
      </div>
  );
}, isEqual);

function Child(props = {}) {
  console.log(`--- re-render ---`);
  return (
      <div>
          <p>number is : {props.number}</p>
      </div>
  );
};

export default function ReactMemo(props = {}) {
    const [step, setStep] = useState(0);
    const [count, setCount] = useState(0);
    const [number, setNumber] = useState(0);

    const handleSetStep = () => {
        setStep(step + 1);
    }

    const handleSetCount = () => {
        setCount(count + 1);
    }

    const handleCalNumber = () => {
        setNumber(count + step);
    }


    return (
        <div>
            <button onClick={handleSetStep}>step is : {step} </button>
            <button onClick={handleSetCount}>count is : {count} </button>
            <button onClick={handleCalNumber}>numberis : {number} </button>
            <hr />
            <Child step={step} count={count} number={number} /> <hr />
            <ChildMemo step={step} count={count} number={number} />
        </div>
    );
}

The following is the output after clicking the buttons from left to right. It can be seen that the component wrapped in memo will only be updated if it satisfies the compare function, while the other component will be re-rendered every time it is clicked.

Nov-24-2022 00-07-23

Starting with React.memo#

Set a breakpoint on ChildMemo, and we will enter the memo function.

export function memo<Props>(
  type: React$ElementType,
  compare?: (oldProps: Props, newProps: Props) => boolean,
) {
  ... Removed dev logic
  const elementType = {
    $$typeof: REACT_MEMO_TYPE,
    type,
    compare: compare === undefined ? null : compare,
  };
  ... Removed dev logic
  return elementType;
}

The logic is simple. It adds the REACT_MEMO_TYPE tag to the wrapped component. This step will have an impact during the construction of the WIP tree (beginWork), which means it will enter the logic for MemoComponent.

How beginWork Handles MemoComponent#

Set a breakpoint on the case MemoComponent in beginWork. Trigger an update, and it will enter the logic for memo (this function is also used during the mount phase).

image
function updateMemoComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
): null | Fiber {
  // When current is null, it corresponds to the mount phase.
  // When current is null, create the corresponding fiber node (createFiberFromTypeAndProps) since there is no object to compare to.
  if (current === null) {
    const type = Component.type;
    ...
    ... Removed dev logic
    const child = createFiberFromTypeAndProps(
      Component.type,
      null,
      nextProps,
      workInProgress,
      workInProgress.mode,
      renderLanes,
    );
    child.ref = workInProgress.ref;
    child.return = workInProgress;
    workInProgress.child = child;
    return child;
  }
	... Removed dev logic
  // Since there is a current node, enter the update logic.
  const currentChild = ((current.child: any): Fiber); // This is always exactly one child
  // renderLanes is the current rendering priority. Only updates with the same priority as renderLanes will be processed in this render.
  // When an update is generated, the lanes of the current update are marked on root.pendingLanes.
  // The highest priority on root.pendingLanes becomes renderLanes.
  // You can check my article on React Hooks: useState for more detailed explanation.
  // The function below actually checks whether there is an update instance on the current fiber with the same priority as renderLanes.
  // If there is, it means that this update must be processed in this render, which means that even if the incoming props have not changed
  // (or the compare function returns true), this component will still be updated.
  // This is the conclusion corresponding to the second point in TLNR.
  const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
    current,
    renderLanes,
  );
  // If there is no update on the current fiber that needs to be processed immediately,
  // enter the logic inside the if statement.
  if (!hasScheduledUpdateOrContext) {
    // This will be the props with resolved defaultProps,
    // unlike current.memoizedProps which will be the unresolved ones.
    const prevProps = currentChild.memoizedProps;
    // Default to shallow comparison
    let compare = Component.compare;
    // If compare does not exist, which means the second parameter of React.memo is not passed, the default shallowEqual will be used.
    // shallowEqual is a shallow comparison that only compares the reference addresses of the two props.
    compare = compare !== null ? compare : shallowEqual;
    if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
      // If compare returns true, it means that the component wrapped in memo can be reused.
      // Therefore, bailoutOnAlreadyFinishedWork is called to reuse the nodes on the current tree without generating nodes repeatedly,
      // thus optimizing performance.
      // I will add the analysis of this function to the appendix later.
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
  }
  // React DevTools reads this flag.
  workInProgress.flags |= PerformedWork;
  // If we reach this point, it means that there is an update that needs to be processed immediately on the current fiber.
  // So we have to generate new nodes.
  const newChild = createWorkInProgress(currentChild, nextProps);
  newChild.ref = workInProgress.ref;
  newChild.return = workInProgress;
  workInProgress.child = newChild;
  return newChild;
}
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.