Пошук уроків, статей та іншого контенту
Розберемо перенесення спільного State до найближчого спільного батьківського компонента для синхронізації дочірніх компонентів.
Підняття стану (lifting state up) — це перенесення спільного state з дочірніх компонентів до їхнього найближчого спільного батьківського компонента.
Цей підхід потрібен, коли два або більше дочірніх компоненти мають:
відображати одні й ті самі дані;
змінювати спільне значення;
залишатися синхронізованими після змін.
Замість того щоб зберігати окремий стан у кожному дочірньому компоненті, ми:
зберігаємо стан у спільному батьківському компоненті;
передаємо дані вниз через props;
передаємо функції для зміни стану вниз через props;
дочірні компоненти викликають ці функції у відповідь на дії користувача.
Схема взаємодії:
стан і функції зміни
↓
дочірні компоненти
подія користувача
↑
callback до батьківського компонентаУявімо компонент із двома полями для введення температури:
температура в градусах Цельсія;
температура в градусах Фаренгейта.
Якщо кожне поле матиме власний незалежний state, компоненти не знатимуть про зміни одне одного. Наприклад, після введення значення в поле Цельсія поле Фаренгейта не оновиться автоматично.
Це призводить до дублювання даних і можливих розбіжностей:
function CelsiusInput() {
const [temperature, setTemperature] = useState('');
// ...
}
function FahrenheitInput() {
const [temperature, setTemperature] = useState('');
// ...
}Тут кожен компонент володіє власною версією температури. React не синхронізує ці два значення автоматично.
Кращий підхід — зберігати одне джерело правди в батьківському компоненті.
Батьківський компонент зберігає:
введене значення;
одиницю вимірювання, у якій це значення було введено.
Потім він передає дочірнім компонентам:
значення для відображення;
обробник зміни значення.
import { useState } from 'react';
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return celsius * 9 / 5 + 32;
}
function tryConvert(value, convert) {
const number = parseFloat(value);
if (Number.isNaN(number)) {
return '';
}
const converted = convert(number);
// Округлення лише для зручнішого відображення.
return Math.round(converted * 1000) / 1000;
}
function TemperatureInput({ scale, temperature, onTemperatureChange }) {
const scaleNames = {
c: 'Цельсій',
f: 'Фаренгейт',
};
function handleChange(event) {
onTemperatureChange(event.target.value);
}
return (
<label>
<span>{scaleNames[scale]}:</span>{' '}
<input
value={temperature}
onChange={handleChange}
placeholder="Введіть температуру"
/>
</label>
);
}
function BoilingVerdict({ celsius }) {
if (celsius === '') {
return null;
}
const value = parseFloat(celsius);
if (value >= 100) {
return <p>Вода закипить.</p>;
}
return <p>Вода ще не закипить.</p>;
}
export default function Calculator() {
const [temperature, setTemperature] = useState('');
const [scale, setScale] = useState('c');
function handleCelsiusChange(value) {
setTemperature(value);
setScale('c');
}
function handleFahrenheitChange(value) {
setTemperature(value);
setScale('f');
}
const celsius =
scale === 'f'
? tryConvert(temperature, toCelsius)
: temperature;
const fahrenheit =
scale === 'c'
? tryConvert(temperature, toFahrenheit)
: temperature;
return (
<main>
<h1>Конвертер температури</h1>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={handleCelsiusChange}
/>
<br />
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={handleFahrenheitChange}
/>
<BoilingVerdict celsius={celsius} />
</main>
);
}У прикладі Calculator є найближчим спільним батьківським компонентом для двох TemperatureInput.
У Calculator стан зберігається в одному місці:
const [temperature, setTemperature] = useState('');
const [scale, setScale] = useState('c');temperature — введене значення.
scale — одиниця, у якій користувач востаннє змінював значення.
Наприклад:
temperature = "100"
scale = "c"Це означає: користувач увів 100 градусів Цельсія.
propsКомпонент TemperatureInput не зберігає власну температуру. Він отримує її від батька:
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={handleCelsiusChange}
/>Тому значення поля повністю контролюється батьківським компонентом.
TemperatureInput отримує функцію onTemperatureChange:
function TemperatureInput({ temperature, onTemperatureChange }) {
function handleChange(event) {
onTemperatureChange(event.target.value);
}
// ...
}Коли користувач вводить нове значення, компонент викликає цю функцію. Сам дочірній компонент не змінює стан напряму — він лише повідомляє батьківський компонент про подію.
Для поля Цельсія викликається:
function handleCelsiusChange(value) {
setTemperature(value);
setScale('c');
}Для поля Фаренгейта:
function handleFahrenheitChange(value) {
setTemperature(value);
setScale('f');
}Після оновлення стану Calculator повторно рендериться та передає обчислені значення обом полям.
У цьому прикладі існує лише одне введене значення:
const [temperature, setTemperature] = useState('');Інше значення обчислюється:
const celsius =
scale === 'f'
? tryConvert(temperature, toCelsius)
: temperature;
const fahrenheit =
scale === 'c'
? tryConvert(temperature, toFahrenheit)
: temperature;Це важливо, тому що зберігати обидва значення в state було б зайвим:
// Не бажаний підхід
const [celsius, setCelsius] = useState('');
const [fahrenheit, setFahrenheit] = useState('');Якщо зберігати обидва значення, потрібно щоразу вручну підтримувати їхню відповідність. Це збільшує кількість логіки та ризик помилок.
Замість цього зберігається мінімальний стан, а похідні значення обчислюються під час рендерингу.
TemperatureInput є контрольованим компонентом, оскільки його значення надходить через props:
<input value={temperature} onChange={handleChange} />У такому компоненті:
value визначає, що показано в полі;
onChange повідомляє про введення користувача;
батьківський компонент оновлює state;
після оновлення нове значення знову передається через props.
Потік даних виглядає так:
state батька
↓
value дочірнього input
onChange input
↓
callback батьківського компонента
↓
оновлення stateПіднімайте стан до батьківського компонента, якщо:
кілька дочірніх компонентів використовують одне значення;
зміна в одному компоненті має впливати на інший;
компонент-батько має реагувати на зміни дочірнього компонента;
потрібно синхронізувати кілька полів або елементів інтерфейсу.
Необов’язково піднімати весь стан якомога вище. Стан варто зберігати в найближчому спільному батьківському компоненті, якому він потрібен.
Наприклад, якщо стан використовується лише одним компонентом, залиште його всередині цього компонента. Якщо стан потрібен двом сусіднім компонентам, підніміть його до їхнього спільного батька.
Підняття стану зазвичай має такий вигляд:
function Parent() {
const [value, setValue] = useState('');
function handleChange(nextValue) {
setValue(nextValue);
}
return (
<>
<Child
value={value}
onChange={handleChange}
/>
<AnotherChild value={value} />
</>
);
}
function Child({ value, onChange }) {
return (
<input
value={value}
onChange={(event) => onChange(event.target.value)}
/>
);
}
function AnotherChild({ value }) {
return <p>Поточне значення: {value}</p>;
}Тут:
Parent володіє станом;
Child може змінювати стан через onChange;
AnotherChild лише відображає поточне значення;
обидва дочірні компоненти працюють з одним джерелом даних.
Не зберігайте одне й те саме значення в кількох дочірніх компонентах:
// Погано: два компоненти мають незалежні копії одного значення
const [valueInFirstChild, setValueInFirstChild] = useState('');
const [valueInSecondChild, setValueInSecondChild] = useState('');Краще перенести стан до спільного батьківського компонента.
propsprops доступні лише для читання. Дочірній компонент не повинен змінювати їх напряму:
function Child({ value }) {
// Так робити не можна:
// value = 'нове значення';
return <p>{value}</p>;
}Для зміни значення дочірній компонент має викликати callback, переданий батьком.
value або onChangeКонтрольоване поле зазвичай потребує обох властивостей:
<input value={value} onChange={handleChange} />Якщо передати лише value, поле стане недоступним для редагування. Якщо логіка компонента має контролювати значення, джерелом цього значення повинен залишатися state батьківського компонента.
stateЯкщо значення можна обчислити з іншого стану, не потрібно дублювати його в state.
// Зайве дублювання
const [firstName, setFirstName] = useState('');
const [fullName, setFullName] = useState('');fullName краще обчислювати під час рендерингу:
const fullName = `${firstName} ${lastName}`;Це зменшує кількість станів, які потрібно синхронізувати.
Підняття стану — це перенесення спільного state до найближчого спільного батьківського компонента.
Батьківський компонент є єдиним джерелом правди.
Дані передаються від батька до дітей через props.
Події передаються від дітей до батька через callback-функції.
Дочірні компоненти не змінюють props напряму.
Контрольовані компоненти отримують значення через value і повідомляють про зміни через onChange.
Похідні дані краще обчислювати, а не дублювати в state.