Підняття стану в React
Підняття стану (lifting state up) - паттерн React, де спільний стан переміщується до найближчого спільного батька компонентів, які його потребують, і передається вниз через props.
Теорія
TL;DR
- Сусідні компоненти не можуть спілкуватися напряму. Батько тримає дані і передає їх вниз.
- Аналогія: двоє дітей, один термостат. Батько тримає ручку і крутить її за запитом.
- Коли стан батька змінюється, всі діти отримують свіжі значення одночасно. Локальні стани розходяться.
- Підніми, якщо 2+ сусіди потребують однакових даних або один input керує іншим.
Швидкий приклад
// ❌ Два inputs, два локальних стани. Вони ніколи не синхронізуються.
function CelsiusInput() {
const [temp, setTemp] = useState(0);
return <input value={temp} onChange={e => setTemp(+e.target.value)} />;
}
function FahrenheitInput() {
const [temp, setTemp] = useState(32); // окремий стан, розходиться
return <input value={temp} onChange={e => setTemp(+e.target.value)} />;
}
// ✅ Батько тримає стан. Діти - controlled components.
function TemperatureConverter() {
const [celsius, setCelsius] = useState(0);
return (
<>
<input value={celsius} onChange={e => setCelsius(+e.target.value)} />
<input
value={Math.round(celsius * 9 / 5 + 32)}
onChange={e => setCelsius((+e.target.value - 32) * 5 / 9)}
/>
</>
);
}
// Змінюєш Celsius → Fahrenheit оновлюється одразу. І навпаки.Головна відмінність
Після підняття дочірні компоненти стають контрольованими (controlled): вони читають з props і викликають батьківські обробники при змінах. Локальний useState зникає. Батько ре-рендериться і одночасно надсилає свіжі значення всім дітям, тому вони не можуть розійтися.
Коли піднімати
- 2+ сусіди потребують однакових даних → підніми до найближчого спільного батька
- Один input керує іншим (Celsius/Fahrenheit) → підніми і обчислюй похідні значення у батьку
- Поля форми потрібно валідувати разом → підніми до форм-контейнера
- Список і фільтр читають з одного масиву → підніми масив
Якщо стан використовується лише одним компонентом, залиш його локальним. Якщо ти передаєш props на 3+ рівні вглиб просто щоб поділитися даними, це prop drilling. Тоді варто підключити Context або стейт-менеджер.
Як це працює всередині React
Коли setState викликається у батьку, React ставить у чергу ре-рендер цього компонента і всього піддерева. Діти отримують свіжі props у фазі рендеру. Якщо значення prop змінилося, React синхронізує DOM через input.value = newValue. Всі діти оновлюються за один прохід, тому не можуть розійтися.
Типові помилки
Поєднання локального стану з пропом
// ❌ Локальний стан перекриває prop. Зміни не доходять до батька.
function Child({ value, onChange }: { value: number; onChange: (v: number) => void }) {
const [local, setLocal] = useState(0); // воює з пропом!
return <input value={local} onChange={e => setLocal(+e.target.value)} />;
}
// ✅ Controlled: використовуй prop напряму
function Child({ value, onChange }: { value: number; onChange: (v: number) => void }) {
return <input value={value} onChange={e => onChange(+e.target.value)} />;
}На практиці це найпоширеніший баг при роботі з цим паттерном. Ти передаєш значення через prop, дитина має локальний useState, і зміни ніколи не доходять до батька. React DevTools показує оновлення prop, але input залишається замороженим.
Підняття занадто високо
// ❌ App ре-рендериться при кожному натисканні клавіші у формі входу
<App formData={formData} setFormData={setFormData}>
<LoginForm />
</App>Підноси тільки до найближчого спільного батька. LoginPage - правильний рівень, не App. Непов'язані компоненти не повинні ре-рендеритися через поле форми.
Мутація стану замість заміни
// ❌ Push мутує масив. React бачить той самий reference. Ре-рендеру не буде.
const addItem = () => { items.push('new'); setItems(items); };
// ✅ Створюй новий масив
const addItem = () => setItems([...items, 'new']);Де зустрічається у реальних проектах
- Приклад з документації React: конвертер температур Celsius/Fahrenheit - звідси і пішов цей паттерн
- TodoMVC: кнопки фільтрів і список todos ділять масив
todos, піднятий доApp - Сторінки пошуку в Next.js: стан запиту піднятий над полем вводу і списком результатів
- Форми входу: перемикач
showPasswordі полеpasswordділять стан у батьківській формі
Питання на співбесіді
Q: Навіщо не використовувати Redux або Context для всього?
A: Redux потребує шаблонного коду (actions, reducers, store), який не окупається для двох сусідів. Спочатку підніми стан. Context підключай при 5+ споживачах або 3+ рівнях вкладення. Redux доречний для стану, що ділиться між багатьма непов'язаними компонентами по всьому додатку.
Q: Як дочірній компонент може передати дані батьку?
A: Через callback prop: onValueChange(data). Дитина викликає його, батько оновлює свій стан. Ось і весь механізм.
Q: Чи викликає підняття стану проблеми з продуктивністю?
A: Рідко. React групує оновлення стану в обробниках подій. Якщо конкретне піддерево ре-рендериться занадто часто, загорни стабільні дочірні компоненти в React.memo. Спочатку виміряй через React DevTools Profiler, потім оптимізуй.
Q: (Senior) Коли підняття стану переростає у prop drilling? Як мігрувати?
A: Приблизно при 10+ полях або 3+ рівнях вкладення. Витягни хук useForm, що повертає { values, updateField }. Загорни піддерево в <FormContext.Provider> і читай через useContext. Стан все ще живе в одному місці, але компоненти отримують доступ без ланцюжків props.
Приклади
Базовий: конвертер температур
function TemperatureConverter() {
const [celsius, setCelsius] = useState(0);
const toFahrenheit = (c: number) => Math.round(c * 9 / 5 + 32);
const toCelsius = (f: number) => (f - 32) * 5 / 9;
return (
<div>
<label>
Celsius:
<input
type="number"
value={celsius}
onChange={e => setCelsius(+e.target.value)}
/>
</label>
<label>
Fahrenheit:
<input
type="number"
value={toFahrenheit(celsius)}
onChange={e => setCelsius(toCelsius(+e.target.value))}
/>
</label>
</div>
);
}
// Вводиш 100 у Celsius → Fahrenheit показує 212 одразу
// Вводиш 32 у Fahrenheit → Celsius показує 0 одразуОдин компонент, одне джерело даних. Обидва inputs читають з celsius і записують через функції конвертації.
Середній рівень: форма входу зі спільним станом
function LoginForm() {
const [formData, setFormData] = useState({
email: '',
password: '',
showPassword: false,
});
const updateField = (field: keyof typeof formData) =>
(e: React.ChangeEvent<HTMLInputElement>) =>
setFormData({ ...formData, [field]: e.target.value });
return (
<form>
<input
value={formData.email}
onChange={updateField('email')}
placeholder="Email"
/>
<input
type={formData.showPassword ? 'text' : 'password'}
value={formData.password}
onChange={updateField('password')}
/>
<label>
<input
type="checkbox"
checked={formData.showPassword}
onChange={e => setFormData({ ...formData, showPassword: e.target.checked })}
/>
Показати пароль
</label>
</form>
);
}
// Ставиш галочку → поле пароля перемикається між text і password
// Всі три поля ділять один об'єкт стану на рівні формиЧекбокс і поле пароля - сусіди. Без підняття чекбокс не міг би впливати на тип поля пароля. Форма тримає все, тому координація тривіальна.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.