React ⚛️
June 12

React. Обновление узлов и мемоизация

В процессе разработки современных веб-приложений производительность часто становится одним из ключевых аспектов, которые волнуют и разработчиков, и пользователей. Пользователи ожидают молниеносного отклика, а разработчики стремятся создать приложения, которые работают быстро и эффективно.

Одним из мощных инструментов, позволяющих достигнуть высокой производительности в React-приложениях, является мемоизация. Мемоизация помогает значительно сократить количество вычислений и, соответственно, обновлений интерфейса, что положительно сказывается на общей скорости и отзывчивости приложения.

В данной статье мы заглянем "под капот" движка React и увидем, как именно происходит обновление узлов. Параллельно рассмотрим и основные принципы мемоизации и её применение в различных типах компонентов.

Что такое мемоизация?

Начнем, пожалуй, с теории. Формально, мемоизацию можно определить как оптимизационную технику, применяемую для увеличения производительности программ за счёт сохранения результатов вызова функции и возвращения сохранённого результата при повторных вызовах с теми же аргументами.

В контексте React мемоизация особенно полезна для предотвращения лишних перерисовок и повышения производительности приложений. Другими словами, она помогает избегать ненужных обновлений компонентов, что делает приложение более реактивным и эффективным. Вторым не менее важным свойством мемоизации является сокращение количества дорогостоящих вычислений. Вместо того, чтобы производить их на каждом рендере компонента, можно "запомнить результат" и пересчитывать его только в случае изменения соответствующих входных параметров.

Где конкретно хранится мемоизированный результат?

Чтобы ответить на этот вопрос, нам понадобится понятие React Fiber. Подробно я описывал эту структуру в статье Детальный React. Реконсиляция, рендеры, Fiber, виртуальное дерево. Если коротко, Fiber - это некая абстрактная сущность движка React, с помощью которой описывается любой обрабатываемый узел React, будь то компонент, хук или host root. У Fiber, кроме прочего, имеются свойства pendingProps, memoizedProps и memoizedState. Именно в этих свойствах и хранится мемоизированный результат. Как именно? Посмотрим далее.

Мемоизируемые компоненты

Хоть принципы мемоизации в React одинаковы для всех сущностей, детали реализации, все таки могут отличаться, в зависимости от типа Fiber. Для понимания процесса нам потребуется пройтись по каждому типу отдельно. И начнем мы с компонентов, как самой "старой" структуры React.

Еще с первых версий React мы привыкли делить компоненты на классовые и функциональные. На самом деле, внутри движка типов компонентов куда больше. Тип компонента является типом Fiber и указывается в свойстве Fiber.tag. В выше указанной статье я приводил полный список типов для версии React 18.2, где в общей сложности насчитывалось 28 типов . В версии 18.3.1 список немного изменился, теперь их всего 26 штук.

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;

Для понимания процессов мемоизации, из этого списка нам понадобятся только три:

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const MemoComponent = 14;

Когда происходит мемоизация?

Прежде чем перейти непосредственно к типам Fiber, стоит разобраться в какой конкретно момент запускается сам процесс мемоизации. В предыдущей статье я выделил четыре фазы обработки узла. Первая фаза begin определяет тип Fiber, в зависимости от которого будет запущен нужный жизненный цикл. Узлы, которые еще небыли смонтированы имеют тип IndeterminateComponent, LazyComponent или IncompleteClassComponent. Для таких Fiber будет запущен процесс монтирования. В остальных же случаях реконсиллер попытается обновить существующий узел. Если быть более конкретным, дерево решений принимается функцией beginWork реконсиллера.

/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L3699

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  ...
  
  switch (workInProgress.tag) {
    case IndeterminateComponent: {
      return mountIndeterminateComponent(
        current,
        workInProgress,
        workInProgress.type,
        renderLanes,
      );
    }
    case LazyComponent: {
      const elementType = workInProgress.elementType;
      return mountLazyComponent(
        current,
        workInProgress,
        elementType,
        renderLanes,
      );
    }
    case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case ClassComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    case HostText:
      return updateHostText(current, workInProgress);
    case SuspenseComponent:
      return updateSuspenseComponent(current, workInProgress, renderLanes);
    case HostPortal:
      return updatePortalComponent(current, workInProgress, renderLanes);
    case ForwardRef: {
      const type = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === type
          ? unresolvedProps
          : resolveDefaultProps(type, unresolvedProps);
      return updateForwardRef(
        current,
        workInProgress,
        type,
        resolvedProps,
        renderLanes,
      );
    }
    case Fragment:
      return updateFragment(current, workInProgress, renderLanes);
    case Mode:
      return updateMode(current, workInProgress, renderLanes);
    case Profiler:
      return updateProfiler(current, workInProgress, renderLanes);
    case ContextProvider:
      return updateContextProvider(current, workInProgress, renderLanes);
    case ContextConsumer:
      return updateContextConsumer(current, workInProgress, renderLanes);
    case MemoComponent: {
      const type = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      // Resolve outer props first, then resolve inner props.
      let resolvedProps = resolveDefaultProps(type, unresolvedProps);
      resolvedProps = resolveDefaultProps(type.type, resolvedProps);
      return updateMemoComponent(
        current,
        workInProgress,
        type,
        resolvedProps,
        renderLanes,
      );
    }
    case SimpleMemoComponent: {
      return updateSimpleMemoComponent(
        current,
        workInProgress,
        workInProgress.type,
        workInProgress.pendingProps,
        renderLanes,
      );
    }
    case IncompleteClassComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return mountIncompleteClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case SuspenseListComponent: {
      return updateSuspenseListComponent(current, workInProgress, renderLanes);
    }
    case ScopeComponent: {
      if (enableScopeAPI) {
        return updateScopeComponent(current, workInProgress, renderLanes);
      }
      break;
    }
    case OffscreenComponent: {
      return updateOffscreenComponent(current, workInProgress, renderLanes);
    }
    case LegacyHiddenComponent: {
      if (enableLegacyHidden) {
        return updateLegacyHiddenComponent(
          current,
          workInProgress,
          renderLanes,
        );
      }
      break;
    }
    case CacheComponent: {
      if (enableCache) {
        return updateCacheComponent(current, workInProgress, renderLanes);
      }
      break;
    }
    case TracingMarkerComponent: {
      if (enableTransitionTracing) {
        return updateTracingMarkerComponent(
          current,
          workInProgress,
          renderLanes,
        );
      }
      break;
    }
  }
  
  ...
}

С точки зрения мемоизации, монитруется компонент или обновляется, большой разницы нет. В конечном итоге, соответствующие методы обновления компонента будут вызваны в любом случае. Разница только в том, будут ли в эти методы переданы предыдущие значения свойств, состояния или зависимостей. Оставлю процесс трансформации монтируемыех типов в обновляемые для следующих публикаций и предлагаю перейти непосредственно к методам обновления конкретных узлов.

ClassComponent

Из наименования понятно, что данный тип присваивается классовым компонентам. На всякий случай, вспомним, как создается классовый компонент.

class MyComponent extends React.Component<{ prop1: string }> {
  render() {
    return (
      <div>
        {this.props.prop1}
      </div>
    );
  }
}

В приведенном выше beginWork обновление классовых компонентов начинается следующим образом.

const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
  workInProgress.elementType === Component
    ? unresolvedProps
    : resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
  current,
  workInProgress,
  Component,
  resolvedProps,
  renderLanes,
);

Первое, что тут бросается в глаза, это константа resolvedProps. Не смотря на аж 5 строк кода, эта константа всего лишь берет свойство Fiber.pendingProps, которое является ничем иным, как объектом с новыми свойствами компонента. Дабы не оставлять неясности, приведу и код функции resolveDefaultProps.

/packages/react-reconciler/src/ReactFiberLazyComponent.new.js#L12

export function resolveDefaultProps(Component: any, baseProps: Object): Object {
  if (Component && Component.defaultProps) {
    // Resolve default props. Taken from ReactElement
    const props = assign({}, baseProps);
    const defaultProps = Component.defaultProps;
    for (const propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
    return props;
  }
  return baseProps;
}

В переводе на человеческий язык, эта функция всего лишь присваивает дефолтные значения свойствам, если таковые были указаны при создании компонента.

class MyComponent extends React.Component<{ color?: string }> {
  static defaultProps = {
    color: 'blue',
  };

  render() {
    return (
      <div>
        {this.props.color}
      </div>
    );
  }
}

<MyComponent />              // <div>blue</div>
<MyComponent color="red" />  // <div>red</div>

Сам же механизм обновления вынесен в функцию updateClassComponent. Приведу её код в сокращенном виде, так как часть функционала в ней отвечает за обработку провайдера контекста и некоторые служебные операции для dev-mode.

/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L1062

function updateClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
) {
  ...
  const instance = workInProgress.stateNode;
  let shouldUpdate;
  if (instance === null) {
    resetSuspendedCurrentOnMountInLegacyMode(current, workInProgress);
    
    // In the initial pass we might need to construct the instance.
    constructClassInstance(workInProgress, Component, nextProps);
    mountClassInstance(workInProgress, Component, nextProps, renderLanes);
    shouldUpdate = true;
  } else if (current === null) {
    // In a resume, we'll already have an instance we can reuse.
    shouldUpdate = resumeMountClassInstance(
      workInProgress,
      Component,
      nextProps,
      renderLanes,
    );
  } else {
    shouldUpdate = updateClassInstance(
      current,
      workInProgress,
      Component,
      nextProps,
      renderLanes,
    );
  }
  const nextUnitOfWork = finishClassComponent(
    current,
    workInProgress,
    Component,
    shouldUpdate,
    hasContext,
    renderLanes,
  );
  ...
  return nextUnitOfWork;
}

Первые два if здесь отвечают за процесс монтирования нового компонента. Как я уже говорил выше, отдельно эти процессы разбирать не будем, так как в конечном итоге цикл все равно прийдет к функции обновления и вся работа с мемоизация будет происходить именно там. А именно, в функции updateClassInstance, её я приведу целиком.

/packages/react-reconciler/src/ReactFiberClassComponent.new.js#L1123

// Invokes the update life-cycles and returns false if it shouldn't rerender.
function updateClassInstance(
  current: Fiber,
  workInProgress: Fiber,
  ctor: any,
  newProps: any,
  renderLanes: Lanes,
): boolean {
  const instance = workInProgress.stateNode;
  
  cloneUpdateQueue(current, workInProgress);
  
  const unresolvedOldProps = workInProgress.memoizedProps;
  const oldProps =
    workInProgress.type === workInProgress.elementType
      ? unresolvedOldProps
      : resolveDefaultProps(workInProgress.type, unresolvedOldProps);
  instance.props = oldProps;
  const unresolvedNewProps = workInProgress.pendingProps;
  
  const oldContext = instance.context;
  const contextType = ctor.contextType;
  let nextContext = emptyContextObject;
  if (typeof contextType === 'object' && contextType !== null) {
    nextContext = readContext(contextType);
  } else if (!disableLegacyContext) {
    const nextUnmaskedContext = getUnmaskedContext(workInProgress, ctor, true);
    nextContext = getMaskedContext(workInProgress, nextUnmaskedContext);
  }
  
  const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
  const hasNewLifecycles =
    typeof getDerivedStateFromProps === 'function' ||
    typeof instance.getSnapshotBeforeUpdate === 'function';
    
  // Note: During these life-cycles, instance.props/instance.state are what
  // ever the previously attempted to render - not the "current". However,
  // during componentDidUpdate we pass the "current" props.
  
  // In order to support react-lifecycles-compat polyfilled components,
  // Unsafe lifecycles should not be invoked for components using the new APIs.
  if (
    !hasNewLifecycles &&
    (typeof instance.UNSAFE_componentWillReceiveProps === 'function' ||
      typeof instance.componentWillReceiveProps === 'function')
  ) {
    if (
      unresolvedOldProps !== unresolvedNewProps ||
      oldContext !== nextContext
    ) {
      callComponentWillReceiveProps(
        workInProgress,
        instance,
        newProps,
        nextContext,
      );
    }
  }
  
  resetHasForceUpdateBeforeProcessing();
  
  const oldState = workInProgress.memoizedState;
  let newState = (instance.state = oldState);
  processUpdateQueue(workInProgress, newProps, instance, renderLanes);
  newState = workInProgress.memoizedState;
  
  if (
    unresolvedOldProps === unresolvedNewProps &&
    oldState === newState &&
    !hasContextChanged() &&
    !checkHasForceUpdateAfterProcessing() &&
    !(
      enableLazyContextPropagation &&
      current !== null &&
      current.dependencies !== null &&
      checkIfContextChanged(current.dependencies)
    )
  ) {
    // If an update was already in progress, we should schedule an Update
    // effect even though we're bailing out, so that cWU/cDU are called.
    if (typeof instance.componentDidUpdate === 'function') {
      if (
        unresolvedOldProps !== current.memoizedProps ||
        oldState !== current.memoizedState
      ) {
        workInProgress.flags |= Update;
      }
    }
    if (typeof instance.getSnapshotBeforeUpdate === 'function') {
      if (
        unresolvedOldProps !== current.memoizedProps ||
        oldState !== current.memoizedState
      ) {
        workInProgress.flags |= Snapshot;
      }
    }
    return false;
  }
  
  if (typeof getDerivedStateFromProps === 'function') {
    applyDerivedStateFromProps(
      workInProgress,
      ctor,
      getDerivedStateFromProps,
      newProps,
    );
    newState = workInProgress.memoizedState;
  }
  
  const shouldUpdate =
    checkHasForceUpdateAfterProcessing() ||
    checkShouldComponentUpdate(
      workInProgress,
      ctor,
      oldProps,
      newProps,
      oldState,
      newState,
      nextContext,
    ) ||
    // TODO: In some cases, we'll end up checking if context has changed twice,
    // both before and after `shouldComponentUpdate` has been called. Not ideal,
    // but I'm loath to refactor this function. This only happens for memoized
    // components so it's not that common.
    (enableLazyContextPropagation &&
      current !== null &&
      current.dependencies !== null &&
      checkIfContextChanged(current.dependencies));
      
  if (shouldUpdate) {
    // In order to support react-lifecycles-compat polyfilled components,
    // Unsafe lifecycles should not be invoked for components using the new APIs.
    if (
      !hasNewLifecycles &&
      (typeof instance.UNSAFE_componentWillUpdate === 'function' ||
        typeof instance.componentWillUpdate === 'function')
    ) {
      if (typeof instance.componentWillUpdate === 'function') {
        instance.componentWillUpdate(newProps, newState, nextContext);
      }
      if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
        instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext);
      }
    }
    if (typeof instance.componentDidUpdate === 'function') {
      workInProgress.flags |= Update;
    }
    if (typeof instance.getSnapshotBeforeUpdate === 'function') {
      workInProgress.flags |= Snapshot;
    }
  } else {
    // If an update was already in progress, we should schedule an Update
    // effect even though we're bailing out, so that cWU/cDU are called.
    if (typeof instance.componentDidUpdate === 'function') {
      if (
        unresolvedOldProps !== current.memoizedProps ||
        oldState !== current.memoizedState
      ) {
        workInProgress.flags |= Update;
      }
    }
    if (typeof instance.getSnapshotBeforeUpdate === 'function') {
      if (
        unresolvedOldProps !== current.memoizedProps ||
        oldState !== current.memoizedState
      ) {
        workInProgress.flags |= Snapshot;
      }
    }
    
    // If shouldComponentUpdate returned false, we should still update the
    // memoized props/state to indicate that this work can be reused.
    workInProgress.memoizedProps = newProps;
    workInProgress.memoizedState = newState;
  }
  
  // Update the existing instance's state, props, and context pointers even
  // if shouldComponentUpdate returns false.
  instance.props = newProps;
  instance.state = newState;
  instance.context = nextContext;
  
  return shouldUpdate;
}

Выглядит довольно громоздко, но что поделать? Именно здесь и происходит вся магия жизненного цикла классовых компонентов. Давайте разбираться по порядку.

const unresolvedOldProps = workInProgress.memoizedProps;
const oldProps =
  workInProgress.type === workInProgress.elementType
    ? unresolvedOldProps
    : resolveDefaultProps(workInProgress.type, unresolvedOldProps);
instance.props = oldProps;
const unresolvedNewProps = workInProgress.pendingProps;

Что такое мы уже видели раньше. Суть этого кода получить две константы: unresolvedOldProps, который сразу записывается в this.props экземпляра компонента и unresolvedNewProps. Эти константы далее будут участвовать в методах жизненного цикла и в самой мемоизации в том числе.

const oldContext = instance.context;
const contextType = ctor.contextType;
let nextContext = emptyContextObject;
if (typeof contextType === 'object' && contextType !== null) {
  nextContext = readContext(contextType);
} else if (!disableLegacyContext) {
  const nextUnmaskedContext = getUnmaskedContext(workInProgress, ctor, true);
  nextContext = getMaskedContext(workInProgress, nextUnmaskedContext);
} 

Следующий блок отвечает за резолв привязанного, компоненту, контекста. Подробно эту часть здесь разбираться мы не будем, но переменные oldContext и nextContext участвует в методах жизненного цикла на ряду с unresolvedOldProps и unresolvedNewProps, поэтому обозначить их стоит.

const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
const hasNewLifecycles =
  typeof getDerivedStateFromProps === 'function' ||
  typeof instance.getSnapshotBeforeUpdate === 'function';
  
// Note: During these life-cycles, instance.props/instance.state are what
// ever the previously attempted to render - not the "current". However,
// during componentDidUpdate we pass the "current" props.

// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for components using the new APIs.
if (
  !hasNewLifecycles &&
  (typeof instance.UNSAFE_componentWillReceiveProps === 'function' ||
    typeof instance.componentWillReceiveProps === 'function')
) {
  if (
    unresolvedOldProps !== unresolvedNewProps ||
    oldContext !== nextContext
  ) {
    callComponentWillReceiveProps(
      workInProgress,
      instance,
      newProps,
      nextContext,
    );
  }
}

Еще один блок, который я так же не могу оставить без внимания. Прежде чем приступить к следующим этапа жизненного цикла, необходимо сначала выполнить метод componentWillReceiveProps(nextProps), если он был определен в компоненте. А интересно здесь то, что метод будет вызван только в том случае, если компонент не использует новое API, т.е. если в нем нет методов getDerivedStateFromProps(props, state) и getSnapshotBeforeUpdate(prevProps, prevState).

const oldState = workInProgress.memoizedState;
let newState = (instance.state = oldState);
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
newState = workInProgress.memoizedState;

И, наконец четвертый блок. Переменные oldState и newState, которые так же участвуют в жизненном цикле компонента. Здесь остановимся чуть поподробнее. Если с oldState все очевидно, он берется из Fiber.memoizedState, то вот newState требует пояснений.

По умолчанию, newState присваивается значение текущего oldState. Далее исполняется цикл updateQueue, инициированный вызовом processUpdateQueue. Приводить код этой функции здесь не буду, так как он тоже довольно большой и вариативный и только отвлечет нас от темы статьи. Главно, что нам нужно тут понять, что эта функция, в конечно итоге сформирует новый State и запишет его в workInProgress.memoizedState.

/packages/react-reconciler/src/ReactFiberClassUpdateQueue.new.js#L458

export function processUpdateQueue<State>(
  ...
  // These values may change as we process the queue.
  if (firstBaseUpdate !== null) {
    ...
    workInProgress.memoizedState = newState;
  }
}

После чего мы и получаем свою переменную newState нашей update-функции.

На это сбор основной информации о компоненте заканчивается и начинается, собственно, сам процесс обновления.

if (
  unresolvedOldProps === unresolvedNewProps &&
  oldState === newState &&
  !hasContextChanged() &&
  !checkHasForceUpdateAfterProcessing() &&
  !(
    enableLazyContextPropagation &&
    current !== null &&
    current.dependencies !== null &&
    checkIfContextChanged(current.dependencies)
  )
) {
  // If an update was already in progress, we should schedule an Update
  // effect even though we're bailing out, so that cWU/cDU are called.
  if (typeof instance.componentDidUpdate === 'function') {
    if (
      unresolvedOldProps !== current.memoizedProps ||
      oldState !== current.memoizedState
    ) {
      workInProgress.flags |= Update;
    }
  }
  if (typeof instance.getSnapshotBeforeUpdate === 'function') {
    if (
      unresolvedOldProps !== current.memoizedProps ||
      oldState !== current.memoizedState
    ) {
      workInProgress.flags |= Snapshot;
    }
  }
  return false;
}

Нет, это пока еще не обновление, как могло показаться на первый взгляд. Прежде чем к нему приступить, реконсиллер, в целях оптимизации исключает лишние обработки. В данном случае, если ссылки на свойства компонента и на его стейт не изменились и не был вызван forceUpdate(), смысла в дальнейшем процессе просто нет и вся функция завершается.

if (typeof getDerivedStateFromProps === 'function') {
  applyDerivedStateFromProps(
    workInProgress,
    ctor,
    getDerivedStateFromProps,
    newProps,
  );
  newState = workInProgress.memoizedState;
}

Вот теперь, если всё же что-то в состоянии компонента поменялось, можно переходить к его обновлению. И первым, что необходимо сделать, это вызвать getDerivedStateFromProps(props, state), так как этот меняет меняет State компонента и, соответственно, переменную newState в update-функции.

const shouldUpdate =
  checkHasForceUpdateAfterProcessing() ||
  checkShouldComponentUpdate(
    workInProgress,
    ctor,
    oldProps,
    newProps,
    oldState,
    newState,
    nextContext,
  ) ||
  // TODO: In some cases, we'll end up checking if context has changed twice,
  // both before and after `shouldComponentUpdate` has been called. Not ideal,
  // but I'm loath to refactor this function. This only happens for memoized
  // components so it's not that common.
  (enableLazyContextPropagation &&
    current !== null &&
    current.dependencies !== null &&
    checkIfContextChanged(current.dependencies));

Наконец, все константы и переменные собраны и теперь можно принять решение о дальнейшем продвижении по жизненному циклу, точнее, будет ли перерисован компонент. А перерисован компонент должен быть только в двух случаях, если значения свойств, стейта или контекст отличают от предыдущих или если был вызван forceUpdate(). Последнее определяется функцией checkHasForceUpdateAfterProcessing(), а вот сравнение значений происходит внутри checkShouldComponentUpdate(). Остановимся на этой функции поподробнее.

/packages/react-reconciler/src/ReactFiberClassComponent.new.js#L305

function checkShouldComponentUpdate(
  workInProgress,
  ctor,
  oldProps,
  newProps,
  oldState,
  newState,
  nextContext,
) {
  const instance = workInProgress.stateNode;
  if (typeof instance.shouldComponentUpdate === 'function') {
    let shouldUpdate = instance.shouldComponentUpdate(
      newProps,
      newState,
      nextContext,
    );
    ...
    return shouldUpdate;
  }
  
  if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    return (
      !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    );
  }
  
  return true;
}

Если убрать из функции служебный dev-mode код, она на удивление довольно простая. Функция предполагает всего три сценария:

  1. Компонент имеет метод shouldComponentUpdate(). В этом случае, решение о том, должен ли быть перерисован компонент возлагается полностью на разработчика.
  2. В случае PureComponent проводится shallow сравнение свойств и стейта (об это подробнее поговорим чуть ниже).
  3. Во всех остальных случаях компонент будет перерисован обязательно.
if (shouldUpdate) {
  // In order to support react-lifecycles-compat polyfilled components,
  // Unsafe lifecycles should not be invoked for components using the new APIs.
  if (
    !hasNewLifecycles &&
    (typeof instance.UNSAFE_componentWillUpdate === 'function' ||
      typeof instance.componentWillUpdate === 'function')
  ) {
    if (typeof instance.componentWillUpdate === 'function') {
      instance.componentWillUpdate(newProps, newState, nextContext);
    }
    if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
      instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext);
    }
  }
  if (typeof instance.componentDidUpdate === 'function') {
    workInProgress.flags |= Update;
  }
  if (typeof instance.getSnapshotBeforeUpdate === 'function') {
    workInProgress.flags |= Snapshot;
  }
} else {
  // If an update was already in progress, we should schedule an Update
  // effect even though we're bailing out, so that cWU/cDU are called.
  if (typeof instance.componentDidUpdate === 'function') {
    if (
      unresolvedOldProps !== current.memoizedProps ||
      oldState !== current.memoizedState
    ) {
      workInProgress.flags |= Update;
    }
  }
  if (typeof instance.getSnapshotBeforeUpdate === 'function') {
    if (
      unresolvedOldProps !== current.memoizedProps ||
      oldState !== current.memoizedState
    ) {
      workInProgress.flags |= Snapshot;
    }
  }
  
  // If shouldComponentUpdate returned false, we should still update the
  // memoized props/state to indicate that this work can be reused.
  workInProgress.memoizedProps = newProps;
  workInProgress.memoizedState = newState;
}

Последним, ну почти последним, этапом является выполнение оставшихся методов жизненного цикла. В случае, если компонент должен быть перерисован, будут выполнены методы:

  1. componentWillUpdate (только если не используется новое API)
  2. UNSAFE_componentWillUpdate (только если не используется новое API)
  3. componentDidUpdate
  4. getSnapshotBeforeUpdate

В противном же случае эти методы вызваны не будут, будут проставлены только нужные служебные флаги, а свойства и стейт будут записаны в memoizedProps и memoizedState соответсвенно.

// Update the existing instance's state, props, and context pointers even
// if shouldComponentUpdate returns false.
instance.props = newProps;
instance.state = newState;
instance.context = nextContext;

return shouldUpdate;

И, наконец, последний этап. Новые свойства, стейт и значение контекста присваиваются экземпляру класса, после чего функция завершается.

Поверхностное (shallow) сравнение

Это понятие уже прозвучало чуть выше. Давайте разберем его детальнее. Мы говорили о том, что такое сравнение происходит в случае реализации PureComponent. На всякий случай вспомним, что такое PureComponent. Воспользуемся для этого примером выше и слегка его изменим.

class MyComponent extends React.PureComponent<{ color?: string }> {
  static defaultProps = {
    color: 'blue',
  };
  
  render() {
    return (
      <div>
        {this.props.color}
      </div>
    );
  }
}

Как вы видите, разница только в том, от какого родителя мы наследуем наш классовый компонент (в исходном варианты мы наследовали от React.Component). Именно в таком компоненте и будет применено shallow сравнение.

Так что же такое shallow сравнение? Пожалуй, лучшим ответом на этот вопрос будет код самой функции shallowEqual, которую мы видели ранее.

/packages/shared/shallowEqual.js#L13

/**
 * Performs equality by iterating through keys on an object and returning false
 * when any key has values which are not strictly equal between the arguments.
 * Returns true when the values of all keys are strictly equal.
 */
function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (is(objA, objB)) {
    return true;
  }
  
  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }
  
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);
  
  if (keysA.length !== keysB.length) {
    return false;
  }
  
  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    const currentKey = keysA[i];
    if (
      !hasOwnProperty.call(objB, currentKey) ||
      !is(objA[currentKey], objB[currentKey])
    ) {
      return false;
    }
  }
  
  return true;
}

Эта утилита условно состоит из двух частей:

  1. Сравнение переменных objA и objB.
  2. Если переменные равны или равны ссылки на объекты, которые они представляют, происходит сравнение каждого из свойств этих объектов.

Само же сравнение осуществляется другой утилитой is().

/packages/shared/objectIs.js#L10

/**
 * inlined Object.is polyfill to avoid requiring consumers ship their own
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
 */
function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
  );
}

const objectIs: (x: any, y: any) => boolean =
  typeof Object.is === 'function' ? Object.is : is;

Которая является ничем иным, как функцией Web API - Object.is(). В случае, если браузер не поддерживыет данный метод Web API, используется встроенный полифил.

Если коротко, два значения счиются одинаковыми, если:

  • оба равны undefined
  • оба равны null
  • оба равны true, либо оба равны false
  • оба являются строками с одинаковой длиной и одинаковыми символами
  • оба являются одним и тем же объектом
  • оба являются BigInt с одинаковым числовым значением
  • оба являются Symbol, которые ссылаются на на один и тот же символ
  • оба являются числами и
    • оба равны +0
    • оба равны -0
    • оба равны NaN
    • либо оба не равны нулю или NaN и оба имеют одинаковое значение

FunctionComponent

С классовыми компонентами разобрались, давайте посмотрим теперь в функциональные.

const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
  workInProgress.elementType === Component
    ? unresolvedProps
    : resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
  current,
  workInProgress,
  Component,
  resolvedProps,
  renderLanes,
);

В самом начале фазы begin инициация обновления функционального компонента ничем не отличается от обновления классового. За исключением самой update-функции.

/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L965

function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps: any,
  renderLanes,
) {
  ...
  
  nextChildren = renderWithHooks(
    current,
    workInProgress,
    Component,
    nextProps,
    context,
    renderLanes,
  );
  hasId = checkDidRenderIdHook();

  ...
    
  if (current !== null && !didReceiveUpdate) {
    bailoutHooks(current, workInProgress, renderLanes);
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
  
  if (getIsHydrating() && hasId) {
    pushMaterializedTreeId(workInProgress);
  }
  
  ...
  
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

А именно, ключевым здесь является запуск отдельного flow под названием renderWithHooks, характерного для функциональных компонентов. Давайте посмотрим, что там происходит.

/packages/react-reconciler/src/ReactFiberHooks.new.js#L374

export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  renderLanes = nextRenderLanes;
  currentlyRenderingFiber = workInProgress;
  
  ...
  
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;
  
  // The following should have already been reset
  // currentHook = null;
  // workInProgressHook = null;
  
  // didScheduleRenderPhaseUpdate = false;
  // localIdCounter = 0;
  
  // TODO Warn if no hooks are used at all during mount, then some are used during update.
  // Currently we will identify the update render as a mount because memoizedState === null.
  // This is tricky because it's valid for certain types of components (e.g. React.lazy)
  
  // Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used.
  // Non-stateful hooks (e.g. context) don't get added to memoizedState,
  // so memoizedState would be null during updates and mounts.
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;
      
  let children = Component(props, secondArg);
  
  // Check if there was a render phase update
  if (didScheduleRenderPhaseUpdateDuringThisPass) {
    // Keep rendering in a loop for as long as render phase updates continue to
    // be scheduled. Use a counter to prevent infinite loops.
    let numberOfReRenders: number = 0;
    do {
      didScheduleRenderPhaseUpdateDuringThisPass = false;
      localIdCounter = 0;
      
      if (numberOfReRenders >= RE_RENDER_LIMIT) {
        throw new Error(
          'Too many re-renders. React limits the number of renders to prevent ' +
            'an infinite loop.',
        );
      }
      
      numberOfReRenders += 1;
      
      // Start over from the beginning of the list
      currentHook = null;
      workInProgressHook = null;
      
      workInProgress.updateQueue = null;
      
      ReactCurrentDispatcher.current = __DEV__
        ? HooksDispatcherOnRerenderInDEV
        : HooksDispatcherOnRerender;
        
      children = Component(props, secondArg);
    } while (didScheduleRenderPhaseUpdateDuringThisPass);
  }
  
  // We can assume the previous dispatcher is always this one, since we set it
  // at the beginning of the render phase and there's no re-entrance.
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;
  
  // This check uses currentHook so that it works the same in DEV and prod bundles.
  // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
  const didRenderTooFewHooks =
    currentHook !== null && currentHook.next !== null;
    
  renderLanes = NoLanes;
  currentlyRenderingFiber = (null: any);
  
  currentHook = null;
  workInProgressHook = null;
  
  didScheduleRenderPhaseUpdate = false;
  // This is reset by checkDidRenderIdHook
  // localIdCounter = 0;
  
  if (didRenderTooFewHooks) {
    throw new Error(
      'Rendered fewer hooks than expected. This may be caused by an accidental ' +
        'early return statement.',
    );
  }
  
  if (enableLazyContextPropagation) {
    if (current !== null) {
      if (!checkIfWorkInProgressReceivedUpdate()) {
        // If there were no changes to props or state, we need to check if there
        // was a context change. We didn't already do this because there's no
        // 1:1 correspondence between dependencies and hooks. Although, because
        // there almost always is in the common case (`readContext` is an
        // internal API), we could compare in there. OTOH, we only hit this case
        // if everything else bails out, so on the whole it might be better to
        // keep the comparison out of the common path.
        const currentDependencies = current.dependencies;
        if (
          currentDependencies !== null &&
          checkIfContextChanged(currentDependencies)
        ) {
          markWorkInProgressReceivedUpdate();
        }
      }
    }
  }
  return children;
}

Функция может показаться большой и сложной, но по сути, она делает всего две вещи. Первое, определяет диспатчер хуков, который будет отвечать за дальнейшее выполнение самих этих хуков. Формально в React существует всего 4 таких диспатчера:

  • ContextOnlyDispatcher
  • HooksDispatcherOnMount
  • HooksDispatcherOnUpdate
  • HooksDispatcherOnRender

На самом же деле, ContextOnlyDispatcher не реализует никаких, он является своего рода диспатчером по умолчанию и выставляется до тех пор, пока движок не определил более релевантный диспатчер текущему компоненту.

Из оставшихся трех, HooksDispatcherOnUpdate и HooksDispatcherOnRender на практике ничем не отличаются и оба ведут на одни и те же реализации update-хуков (updateMemo, updateCallback и т.д.). Выделение двух разных диспатчеров здесь носит исключительно логический характер, на случай, если разработчикам React когда-нибудь понадобится сделать разные версии хуков под разные фазы. К в случае в HooksDispatcherOnMount который ведет к отедельным реализациям mount-хуков (mountMemo, mountCallback, и т.д.).

Про хуки и их провайдеры поговорим чуть ниже. Пока зафиксируем только тот факт, что у каждого хука имеется две версии: mount и update. И на текущем этапе важно определить, какую версию хуков движок будет исполнять. Делается следующим образом.

ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;

Где current - ссылка на Fiber.alternate. Если она равно null, значит компонент еще ни раз не был создан. Аналогично, если Fiber.alternate.memoizedState пустой, то хуки еще в этом компоненте еще ни разу не выполнялись. В обоих случаях будет применен диспатчер HooksDispatcherOnMount. В противном же случае - HooksDispatcherOnUpdate.

Второе, что делает функция - создает компонент посредством let children = Component(props, secondArg) и повторяет этот процесс в цикле, до тех пор, пока в текущей фазе не будут выполнены все запланированные обновления. И да, именно здесь и выбрасывается то самое исключение Too many re-renders. React limits the number of renders to prevent an infinite loop., если количество обновлений превысит 25 итераций.

HooksDispatcherOnMount

Код диспатчера выглядит следующим образом

/packages/react-reconciler/src/ReactFiberHooks.new.js#L2427

const HooksDispatcherOnMount: Dispatcher = {
  readContext,
  
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useInsertionEffect: mountInsertionEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useMutableSource: mountMutableSource,
  useSyncExternalStore: mountSyncExternalStore,
  useId: mountId,
  
  unstable_isNewReconciler: enableNewReconciler,
};
if (enableCache) {
  (HooksDispatcherOnMount: Dispatcher).getCacheSignal = getCacheSignal;
  (HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
  (HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh;
}

Все mount-функции хуков мы рассматривать не будем, важнее понять суть этих функций. Её и посмотрим на примере mountMemo.

/packages/react-reconciler/src/ReactFiberHooks.new.js#L1899

function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

Как можно видеть, код фука до неприличия прост. Сначала определяется ссылка на Fiber. Затем производится вычисление значения, это происходит в пользовательском колбэке nextCreate. Далее, вычисленное значение и массив зависимостей сохраняются в Fiber.memoizedState. Вот она, мемоизация!

HooksDispatcherOnUpdate

Теперь заглянем в диспатчер HooksDispatcherOnUpdate.

/packages/react-reconciler/src/ReactFiberHooks.new.js#L2454

const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,
  
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useInsertionEffect: updateInsertionEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useMutableSource: updateMutableSource,
  useSyncExternalStore: updateSyncExternalStore,
  useId: updateId,
  
  unstable_isNewReconciler: enableNewReconciler,
};
if (enableCache) {
  (HooksDispatcherOnUpdate: Dispatcher).getCacheSignal = getCacheSignal;
  (HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType;
  (HooksDispatcherOnUpdate: Dispatcher).useCacheRefresh = updateRefresh;
}

Как я и говорил ранее, этот диспатчер отличается от предыдущего тем, что ведет на update-версии тех же самы хуков. И раз уж в предыдущем примере мы смотрели в mountMemo, в этот раз давайте заглянем в updateMemo.

/packages/react-reconciler/src/ReactFiberHooks.new.js#L1910

function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    // Assume these are defined. If they're not, areHookInputsEqual will warn.
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

А вот тут уже имеет место эффект памяти, который инициировали на этапе монтирования. Теперь, прежде чем приступить к вычислениям, хук возьмет предыдущие зависимости из Fiber.memoizedState[1] и сравнит их с текущими посредством функции areHookInputsEqual(nextDeps, prevDeps).

/packages/react-reconciler/src/ReactFiberHooks.new.js#L327

function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
  ...
  
  if (prevDeps === null) {
    if (__DEV__) {
      console.error(
        '%s received a final argument during this render, but not during ' +
          'the previous render. Even though the final argument is optional, ' +
          'its type cannot change between renders.',
        currentHookNameInDev,
      );
    }
    return false;
  }
  
  ...
  
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

Которая в свою очередь, проводит поверхностное сравнение зависимостей с помощью всё того же Object.is.

MemoComponent

Настало время поговорить о третьем интересующим нас типе узла - MemoComponent. Такой узел можно создать с помощью React.memo.

const MyComponent = React.memo<{ color?: string }>(({ color }) => {
  return <div>{color}</div>;
});

Такой компонент будет перерисовываться только в том случае, если изменились переданные ему свойства. Выглядит это следующим образом:

const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
// Resolve outer props first, then resolve inner props.
let resolvedProps = resolveDefaultProps(type, unresolvedProps);

resolvedProps = resolveDefaultProps(type.type, resolvedProps);
return updateMemoComponent(
  current,
  workInProgress,
  type,
  resolvedProps,
  renderLanes,
);

Update-функция для таких компонентов называется updateMemoComponent. Функция довольно длинная, приведу только ту её часть, которая нам нужна в рамках этой статьи.

/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L452

function updateMemoComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
): null | Fiber {
  ...
  
  const currentChild = ((current.child: any): Fiber); // This is always exactly one child
  const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
    current,
    renderLanes,
  );
  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;
    compare = compare !== null ? compare : shallowEqual;
    if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
  }
  // React DevTools reads this flag.
  workInProgress.flags |= PerformedWork;
  const newChild = createWorkInProgress(currentChild, nextProps);
  newChild.ref = workInProgress.ref;
  newChild.return = workInProgress;
  workInProgress.child = newChild;
  return newChild;
}

Собственно, вся суть этого кода сводится к вызову функции сравнения compare(prevProps, nextProps), где compare может быть как пользовательской функцией (второй аргумент React.memo) или shallowEqual по умолчанию, её мы уже видели выше.

Заключение

В этой статье мы заглянули "под капот" движка React и посмотрели на механизмы монтирования и обновления узлов. Одним из важнейших механизмов React, участвующим в этих процессах является меомизация. Её мы разобрали на примерах трех типов Fiber: ClassComponent, FunctionComponent и MemoComponent.

И покуда мы теперь лучше понимаем эти процессы, самое время сделать некоторые выводы.

  • Будь то класс, функция или зависимости хука, React в качестве фукнции-сравнения использует один и тот же подход - метод Object.is() Web API или его полифил.
  • Каждый хук имеет две версии реализации, mount и update. При чем update, как правило, более сложная функция, так как ей требуется произвести сравнение зависимостей. Разработчик не можем повлиять на то, какую именно версию хука будет использовать движок. Mount-версия сработает только при первом монтировании компонента.
  • Так как React, при обработке хука сравнивает каждую зависимость отдельно, не стоит добавлять лишние зависимости. Кроме того, сами зависимости хранятся в памяти, большое их количество может сказать потреблении ресурсов. Другими словами,
const value = useMemo(() => {
  return a + b
}, [a, b]);

этот код будет чуть более производителен, чем следующий

const value = useMemo(() => {
  return a + b
}, [a, b, c]);

Аналогично,

const SOME_CONST = "some value";

const MyComponent = ({ a, b }) => {
  // этот хук более производителен
  const value1 = useMemo(() => {
    return a + SOME_CONST
  }, [a]);

  // чем этот
  const value2 = useMemo(() => {
    return b + SOME_CONST
  }, [b, SOME_CONST]);  

  return null;
}
  • По той же причине стоит адекватно оценивать необходимость в мемоизации в целом, например,
const isEqual = useMemo(() => {
  return a === b
}, [a, b]);

Не даст никакого прироста в производительности. Даже наоборот, движок будет вынужден каждый раз сравнивать prevDeps.a === currDeps.a и prevDeps.b === currDeps.b. К тому же, в коде появляется дополнительная функция, которая будет потреблять свой ресурс.

  • Предыдущий тезис касается и React.memo. Разработчик должен понимать, будет ли эффект от мемоизации выше накладных расходов на его её поддержание.


Мои телеграмм-каналы:

EN - https://t.me/frontend_almanac
RU - https://t.me/frontend_almanac_ru

English version: https://blog.frontend-almanac.com/react-memoization