Коли два компоненти мають показувати чи змінювати одні й ті самі дані, стан не може жити в кожному з них окремо - копії розійдуться. Його переносять у найближчого спільного предка й передають вниз через props.
Було - кожна панель сама вирішує, чи відкрита:
function Panel({ title, children }) {
const [isOpen, setIsOpen] = useState(false);
// ...
}
Вимога «відкрита лише одна панель одночасно» так не реалізується: панелі не знають одна про одну.
Стало - стан у батька, панелі керовані:
function Accordion() {
const [openId, setOpenId] = useState('about');
return (
<>
<Panel title="Про нас" isOpen={openId === 'about'} onOpen={() => setOpenId('about')}>...</Panel>
<Panel title="Доставка" isOpen={openId === 'delivery'} onOpen={() => setOpenId('delivery')}>...</Panel>
</>
);
}
function Panel({ title, isOpen, onOpen, children }) {
return (
<section>
<button onClick={onOpen}>{title}</button>
{isOpen && children}
</section>
);
}
Panel став керованим (controlled): ним керує батько через props. Компонент зі своїм внутрішнім станом - некерований (uncontrolled).
Принцип «одного джерела правди»: для кожного шматка стану є один компонент-власник. Інші отримують значення через props і повідомляють про зміни через колбеки.
Куди піднімати:
- до найближчого спільного предка всіх, хто використовує дані, - не вище. Стан занадто високо змушує перерендерюватися велику частину дерева й ускладнює компоненти;
- якщо спільний предок дуже далеко і дані треба передавати через багато рівнів («prop drilling»), - спершу композиція (передати готовий JSX через
children), потім Context.
Зворотний процес теж корисний: якщо стан використовується лише в одному компоненті, його варто опустити туди (state colocation) - менше зайвих рендерів і простіші батьки.
Типова помилка - дублювати стан: копіювати props у локальний стан дитини (useState(props.value)). Копія не оновиться, коли батько змінить значення. Або компонент керований (значення з props), або некерований (власний стан з початковим значенням), - змішування дає розсинхронізацію.