Внутреннее устройство React: Какой useEffect запускается первым?

Это не особенно очевидно, но дочерний useEffect будет выполняться раньше родительского. Давайте разберёмся, почему.

useEffect — один из наиболее часто используемых хуков в сообществе React. Независимо от того, сколько у вас опыта работы с React, вы наверняка его уже использовали.

Но приходилось ли сталкиваться с ситуациями, когда хуки useEffect запускаются в неожиданном порядке, если задействовано несколько слоёв компонентов?

Давайте начнём с небольшого теста. Каким будет правильный порядок следования сообщений console.log в консоли?

function Parent({ children }) {
console.log("Parent is rendered");
useEffect(() => {
console.log("Parent committed effect");
}, []);

return <div>{children}</div>;
}

function Child() {
console.log("Child is rendered");
useEffect(() => {
console.log("Child committed effect");
}, []);

return <p>Child</p>;
}

export default function App() {
return (
<Parent>
<Child />
</Parent>
);
}
Ответ
// initial render
Parent is rendered
Child is rendered

// useEffects
Child committed effect
Parent committed effect

Если ответили правильно — отличная работа! Если нет, не волнуйтесь — большинство разработчиков React тоже ошибаются. На самом деле, на официальном сайте React нет чёткой документации или объяснений.

Давайте разберёмся, почему дочерние компоненты отображаются последними, а их эффекты коммитятся первыми. Мы рассмотрим, как и когда React рендерит компоненты и коммитит эффекты (useEffect). Затронем несколько внутренних концепций React, таких как архитектура React Fiber и её алгоритм обхода.

Обзор внутреннего устройства React

Согласно официальной документации React, весь жизненный цикл компонентов React можно условно разделить на 3 фазы: ТриггерРендерКоммит

Срабатывание рендеринга

Рендеринг

Коммит в DOM

React Fiber Tree

Прежде чем погрузиться в алгоритм обхода, необходимо понять архитектуру React Fiber. Я постараюсь сделать это введение понятным для новичков.

Внутри React использует древовидную структуру данных под названием Fiber Tree для представления иерархии компонентов и отслеживания обновлений.

Дерево React Fiber
Дерево React Fiber

На приведённой выше диаграмме видно, что Fiber Tree — это не совсем отображение DOM-дерева один к одному. Оно содержит дополнительную информацию, помогающую React эффективнее управлять рендерингом.

Каждый узел в дереве называется Fiber Node. Существуют различные типы Fiber Node, например HostComponent, ссылающийся на собственный элемент DOM, как <div> или <p> на диаграмме. FiberRootNode является корневым узлом и при каждом новом рендере будет указывать на другой узел HostRoot.

Каждый Fiber Node содержит такие свойства, как props, state и самое главное:

  1. child — Дочерний Fiber.
  2. sibling — Родственный Fiber.
  3. return — Возвращаемое значение Fiber — родительский Fiber.

Эта информация позволяет React формировать дерево.

Каждый раз, когда происходит обновление состояния, React строит новое дерево Fiber Tree и сравнивает его со старым.

Как выполняется обход Fiber Tree

Как правило, React использует один и тот же алгоритм обхода Fiber Tree во многих случаях.

Алгоритм обхода Fiber Tree
Алгоритм обхода Fiber Tree

Анимация выше показывает, как React обходит Fiber Tree. Обратите внимание, что каждый узел обходится дважды. Правило простое:

  1. Спускается вниз.
  2. В каждой Fiber Node React проверяет
    1. Если есть дочерний узел, перемещается к нему.
    2. Если дочерних узлов нет, снова перемещается к текущему узлу. Затем,
      1. Если есть родственный узел, перемещается к нему.
      2. Если нет родственного узла, перемещается к его родительскому узлу.

Этот алгоритм обхода гарантирует, что каждый узел будет обойдён дважды.

Теперь давайте вернёмся к приведённому в начале тесту.

Фаза рендеринга

Фаза рендеринга
Фаза рендеринга

React обходит Fiber Tree и рекурсивно выполняет два шага на каждом Fiber Node:

В конце фазы рендеринга генерируется новое Fiber Tree с обновлёнными узлами DOM. В этот момент ничего ещё не было закоммичено в реальный DOM. Фактические мутации DOM произойдут в фазе коммита.

Фаза коммита

В фазе коммита происходят фактические мутации DOM и смывание эффектов (useEffect). Схема обхода остаётся прежней, но мутации DOM и смывание эффектов обрабатываются в отдельных проходах.

В этом разделе мы пропустим мутации DOM и сконцентрируемся на прохождении эффекта смывания.

Коммит эффектов

В React используется тот же алгоритм обхода. Однако вместо того, чтобы проверять, есть ли у узла дочерние элементы, он проверяет, есть ли у него поддерево — что имеет смысл, поскольку только компоненты React могут содержать хуки useEffect. Узел DOM типа <p> не будет содержать никаких хуков React.

На первом этапе ничего не происходит, но на втором этапе он коммитит эффекты.

Коммит эффектов
Коммит эффектов

Этот обход в глубину объясняет, почему дочерние эффекты запускаются раньше родительских. Это и есть первопричина.

Теперь давайте рассмотрим ещё один тест. Теперь для вас результат должен быть более осмысленным.

function Parent({ children }) {
console.log("Parent is rendered");
useEffect(() => {
console.log("Parent committed effect");
}, []);

return <div>{children}</div>;
}

function Child() {
console.log("Child is rendered");
useEffect(() => {
console.log("Child committed effect");
}, []);

return <p>Child</p>;
}

function ParentSibling() {
console.log("ParentSibling is rendered");
useEffect(() => {
console.log("ParentSibling committed effect");
}, []);

return <p>Parent's Sibling</p>;
}

export default function App() {
return (
<>
<Parent>
<Child />
</Parent>
<ParentSibling />
</>
);
}
Ответ
// Initial render
Parent is rendered
Child is rendered
ParentSibling is rendered

// useEffects
Child committed effect
Parent committed effect
ParentSibling committed effect

Во время фазы коммита:

Фаза коммита
Фаза коммита

Теперь должно быть понятно, почему дочерние эффекты стираются раньше, чем родительские, во время фазы коммита.

Понимание того, как и когда React коммитит хуки useEffect, поможет избежать мелких багов и неожиданного поведения — особенно при работе со сложными структурами компонентов.

Добро пожаловать во внутреннее устройство React!

Комментарии


Дополнительные материалы

Предыдущая Статья

Проблемы преобразования значений в строки в JavaScript

Следующая Статья

Error.isError(): Лучший способ проверки типов ошибки в JavaScript