先日「React v16.6.0: lazy, memo and contextType」にていくつかの新機能が発表されました。この中のReact.memoが個人的に嬉しい機能だったので、軽く調べてみました。

React.memoとは何か

Class components can bail out from rendering when their input props are the same using PureComponent or shouldComponentUpdate.

ReactではComponentの再レンダリング回数を最小限にしパフォーマンスを上げる方法として、shouldComponentUpdateやPure Componentがあります。ですがFunctional Componentを使う場合はこの仕組みを利用できませんでした。これを可能にしてくれるのがReact.memoです。

const MyFunctionalComponent = ({ name }) => <div>Hello { name }</div>;
const MemoComponent = React.memo(MyFunctionalComponent);

MemoComponentはpropsが変化したときのみ再レンダリングされます。

ソースから読み解くReact.memoの挙動

memo.jsの主要部分を見てみます。

export default function memo<Props>(
  type: React$ElementType,
  compare?: (oldProps: Props, newProps: Props) => boolean,
) {
  if (__DEV__) {
    if (!isValidElementType(type)) {
      warningWithoutStack(
        false,
        'memo: The first argument must be a component. Instead ' +
          'received: %s',
        type === null ? 'null' : typeof type,
      );
    }
  }
  return {
    $$typeof: REACT_MEMO_TYPE,
    type,
    compare: compare === undefined ? null : compare,
  };
}

memo関数は2つの引数typecompareを受取っています。typeはComponentで、compoareはshouldComponentUpdateに相当するもののように見えます。返り値のオブジェクトは、2つの引数と$$typeof: REACT_MEMO_TYPEというプロパティを持つ新しいオブジェクトです。

つまりReact.memoがしていることは「受取ったComponentにREACT_MEMO_TYPEという印をつけている」感じですね。意外にシンプルです。さて、これがどのように利用されるのでしょうか。

ReactFiber.jsのソースを見てみます。typeをもとにFiberインスタンスを生成するcreateFiberFromTypeAndProps関数から追っていきます。

REACT_MEMO_TYPEの利用箇所 - ReactFiber.js#L473
export function createFiberFromTypeAndProps(
  type: any, // React$ElementType
  key: null | string,
  pendingProps: any,
  owner: null | Fiber,
  mode: TypeOfMode,
  expirationTime: ExpirationTime,
): Fiber {
  // 略

  switch (type.$$typeof) {
    case REACT_MEMO_TYPE:
    fiberTag = MemoComponent;
  }

  // 略

  fiber = createFiber(fiberTag, pendingProps, key, mode);
}

$$typeof: REACT_MEMO_TYPEが付与されたオブジェクトはMemoComponentとしてFiberインスタンスが生成されます。

Componentライフサイクル実行前 - ReactFiberBeginWork.js#L1693
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
): Fiber | null {
  // 略

  switch (workInProgress.tag) {
    case MemoComponet:
      const type = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps = resolveDefaultProps(type.type, unresolvedProps);
      return updateMemoComponent(
        current,
        workInProgress,
        type,
        resolvedProps,
        updateExpirationTime,
        renderExpirationTime,
      );
    }
  }
}

Componentの種類に応じたUpdate処理を呼び出しています。

MemoComponentのupdate - ReactFiberBeginWork.js#L235
function updateMemoComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  updateExpirationTime,
  renderExpirationTime: ExpirationTime,
): null | Fiber {
  // 略

  // compareがないMemoComponentはシンプルにpropsをshallowEqualで比較してupdateしている
  if (isSimpleFunctionComponent(type) && Component.compare === null) {  
    workInProgress.tag = SimpleMemoComponent;
    workInProgress.type = type;
    return updateSimpleMemoComponent(
      current,
      workInProgress,
      type,
      nextProps,
      updateExpirationTime,
      renderExpirationTime,
    );
  }

  // 略

  // compareがある場合はそれを使い、ない場合はshallowEqualを使い比較してupdateしている
  let compare = Component.compare;
  compare = compare !== null ? compare : shallowEqual;
  if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
    return bailoutOnAlreadyFinishedWork(
      current,
      workInProgress,
      renderExpirationTime,
    );
  }
  let newChild = createWorkInProgress(
    currentChild,
    nextProps,
    renderExpirationTime,
  );
  newChild.ref = workInProgress.ref;
  newChild.return = workInProgress;
  workInProgress.child = newChild;
  return newChild;
}

compareがある場合はcompareを使ってpropsを比較し、ない場合はshallowEqualでpropsを比較しupdateしています。

まとめ

ざっと見た感じ、

  • React.memo(Component) … 引数のComponentをPure Component化する
  • React.memo(Component, compare) … 引数のComponentにshouldComponentUpdateの挙動をを付与する

という感じでした。
Stateが不要な場面ではFunctional Componentを使いたいので、このUpdateは嬉しい限りです。