React16.6の新機能 React.memo のコードリーディング
先日「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つの引数type
とcompare
を受取っています。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は嬉しい限りです。