Внутреннее устройство 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 фазы: Триггер
→ Рендер
→ Коммит
Срабатывание рендеринга
- Первоначальный рендер компонента или обновление состояния с помощью
setState
. - Обновление состояния ставится в очередь и планируется к обработке React Scheduler.
Рендеринг
- React вызывает компонент и работает над обновлением состояния.
- React выверяет состояние и помечает его как «грязное» для фазы коммита.
- Создаётся новый внутренний узел DOM.
Коммит в DOM
- Применяются фактические манипуляции с DOM.
- Запускаются эффекты (
useEffect
,useLayoutEffect
).
React Fiber Tree
Прежде чем погрузиться в алгоритм обхода, необходимо понять архитектуру React Fiber. Я постараюсь сделать это введение понятным для новичков.
Внутри React использует древовидную структуру данных под названием Fiber Tree для представления иерархии компонентов и отслеживания обновлений.

На приведённой выше диаграмме видно, что Fiber Tree — это не совсем отображение DOM-дерева один к одному. Оно содержит дополнительную информацию, помогающую React эффективнее управлять рендерингом.
Каждый узел в дереве называется Fiber Node. Существуют различные типы Fiber Node, например HostComponent
, ссылающийся на собственный элемент DOM, как <div>
или <p>
на диаграмме. FiberRootNode
является корневым узлом и при каждом новом рендере будет указывать на другой узел HostRoot
.
Каждый Fiber Node содержит такие свойства, как props
, state
и самое главное:
child
— Дочерний Fiber.sibling
— Родственный Fiber.return
— Возвращаемое значение Fiber — родительский Fiber.
Эта информация позволяет React формировать дерево.
Каждый раз, когда происходит обновление состояния, React строит новое дерево Fiber Tree и сравнивает его со старым.
Как выполняется обход Fiber Tree
Как правило, React использует один и тот же алгоритм обхода Fiber Tree во многих случаях.

Анимация выше показывает, как React обходит Fiber Tree. Обратите внимание, что каждый узел обходится дважды. Правило простое:
- Спускается вниз.
- В каждой Fiber Node React проверяет
- Если есть дочерний узел, перемещается к нему.
- Если дочерних узлов нет, снова перемещается к текущему узлу. Затем,
- Если есть родственный узел, перемещается к нему.
- Если нет родственного узла, перемещается к его родительскому узлу.
Этот алгоритм обхода гарантирует, что каждый узел будет обойдён дважды.
Теперь давайте вернёмся к приведённому в начале тесту.
Фаза рендеринга

React обходит Fiber Tree и рекурсивно выполняет два шага на каждом Fiber Node:
- На первом шаге React вызывает компонент — именно здесь выполняется выражение ё. React выверяет и помечает Fiber как «грязный», если
state
илиprops
изменились, подготавливая его к фазе коммита. - На втором шаге React создаёт новый узел DOM.
В конце фазы рендеринга генерируется новое 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!