Что такое «подъём состояния» (lifting state up) и когда он необходим?
Подъём состояния — перемещение общего state в ближайшего общего родителя двух компонентов, которым нужно одно и то же значение. Родитель передаёт state через props вниз и колбэк для обновления наверх. Это базовый способ синхронизировать два компонента без сторонних библиотек.
Зачем поднимать состояние
Если два компонента-сиблинга показывают или редактируют одно и то же значение, дублирование useState в каждом из них немедленно приводит к рассинхрону: изменение в одном не отражается в другом. Решение — убрать state из обоих, переместить его в ближайшего общего предка и передать вниз через props. Один источник правды, два потребителя.
Когда это нужно
- Два сиблинга должны быть согласованы: вкладки + панель содержимого, форма + предпросмотр, фильтр + список результатов.
- Один компонент инициирует изменение, другой реагирует на него.
- Родитель должен валидировать или агрегировать значения нескольких дочерних полей.
Пример: конвертер температур
import { useState } from "react";
type Scale = "c" | "f";
function TempInput({
scale,
value,
onChange,
}: {
scale: Scale;
value: string;
onChange: (v: string) => void;
}) {
return (
<label>
{scale.toUpperCase()}:
<input
type="number"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</label>
);
}
export function TempConverter() {
// Единый источник правды — хранится в родителе
const [celsius, setCelsius] = useState("");
const fahrenheit =
celsius === "" ? "" : String((Number(celsius) * 9) / 5 + 32);
function handleFahrenheitChange(v: string) {
setCelsius(v === "" ? "" : String(((Number(v) - 32) * 5) / 9));
}
return (
<>
<TempInput scale="c" value={celsius} onChange={setCelsius} />
<TempInput scale="f" value={fahrenheit} onChange={handleFahrenheitChange} />
</>
);
}
Оба TempInput — контролируемые компоненты: они не хранят state, только отображают значение и сообщают родителю о желании его изменить. Родитель пересчитывает производное значение синхронно при каждом изменении.
Когда подъём избыточен
- Данные нужны только одному компоненту — оставьте state внутри.
- Расстояние «вверх» превышает 3 уровня (prop drilling) — рассмотрите
createContextили Zustand/Redux. - Значение меняется очень часто (каждый keystroke) и нужно только локально — подъём вызывает лишние ре-рендеры родителя.
Подводные камни
- Prop drilling: когда state поднят слишком высоко и передаётся через 4–5 промежуточных компонентов, каждый из которых не использует его — это сигнал к Context или внешнему store.
- Контролируемый инпут требует синхронной передачи value обратно; задержка (async setState) приводит к «прыгающему» курсору и потере символов.
- Не путайте подъём с глобальным state: для двух соседних компонентов подъём дешевле любого Redux/Zustand/Jotai — не усложняйте без причины.
- Подъём увеличивает связность: родитель начинает знать о деталях реализации детей. Если дети меняются независимо, предпочтительна инверсия зависимости через render props или children.
- При подъёме асинхронного state (результат fetch) в общий предок следите, чтобы отмена запроса (
AbortController) тоже управлялась на уровне предка. - Избегайте поднимать ref вместо state:
useRefне вызывает ре-рендер, поэтому сиблинги не узнают об изменении.
Common mistakes
- Дублировать одинаковое state в двух сиблингах вместо подъёма.
- Поднимать state выше, чем нужно, и заставлять перерисовываться полдерева.
- Поднимать state ради «архитектурной чистоты», когда хватит контекста.
What the interviewer is testing
- Может ли решить задачу синхронизации двух инпутов.
- Понимает ли, когда подъём избыточен.
- Знает ли альтернативу через композицию children.