Совместное использование состояния между компонентами
Иногда требуется, чтобы состояние двух компонентов всегда изменялось вместе. Для этого нужно удалить состояние из обоих компонентов, переместить его в ближайшего общего родителя и передать его им через пропсы. Это называется подъемом состояния вверх и является одним из наиболее распространенных приемов при написании кода на React.
You will learn
- Как использовать одно состояние между компонентами, подняв его вверх
- Что такое управляемые и неуправляемые компоненты
Подъём состояния на примере
В этом примере родительский компонент Accordion
рендерит два отдельных компонента Panel
:
Accordion
Panel
Panel
Каждый компонент Panel
имеет булевое состояние isActive
, которое определяет, будет ли его содержимое видимым.
Нажмите кнопку Показать на обеих панелях:
import { useState } from 'react'; function Panel({ title, children }) { const [isActive, setIsActive] = useState(false); return ( <section className="panel"> <h3>{title}</h3> {isActive ? ( <p>{children}</p> ) : ( <button onClick={() => setIsActive(true)}> Показать </button> )} </section> ); } export default function Accordion() { return ( <> <h2>Алматы, Казахстан</h2> <Panel title="Подробнее"> Алматы с населением около 2 миллионов человек является крупнейшим городом Казахстана. С 1929 по 1997 год этот город был столицей. </Panel> <Panel title="Этимология"> Название происходит от <span lang="kk-KZ">алма</span>, казахского слова, означающего "яблоко", и часто переводится как "полное яблок". На самом деле, регион, прилегающий к Алматы, считается прародиной яблони, а дикий <i lang="la">Malus sieversii</i> считается вероятным кандидатом на роль предка современного домашнего яблока. </Panel> </> ); }
Обратите внимание, что нажатие кнопки на одной панели не влияет на другую панель—они независимы.
А теперь предположим, что вы хотите изменить поведение так, чтобы в любой момент времени была раскрыта только одна панель. В таком случае раскрытие второй панели должно привести к сворачиванию первой. Как бы вы это сделали?
Чтобы согласовать поведение этих двух панелей, вам нужно “поднять их состояние” в родительский компонент в три шага:
- Удалите состояние из дочерних компонентов.
- Передайте данные хардкодом из общего родителя.
- Добавьте состояние в общего родителя и передайте его вместе с обработчиками событий.
Это позволит компоненту Accordion
управлять обеими панелями и раскрывать только по одной за раз.
Шаг 1: Удалить состояние из дочерних компонентов
Вы передадите управление значением isActive
родительскому компоненту Panel
. Это означает, что родительский компонент будет передавать значение isActive
через проп, вместо того, чтобы хранить это состояние в Panel
. Начните с удаления этой строки из компонента Panel
:
const [isActive, setIsActive] = useState(false);
И вместо нее, добавьте isActive
в список пропсов Panel
:
function Panel({ title, children, isActive }) {
Теперь родитель компонента Panel
может управлять isActive
передавая его вниз как проп. С другой стороны, компонент Panel
теперь не имеет контроля над значением isActive
—теперь оно зависит от родительского компонента!
Шаг 2: Передать данные хардкодом из общего родителя
Чтобы поднять состояние, вам необходимо определить ближайшего общего родителя для обоих дочерних компонентов, которые вы хотите скоординировать:
Accordion
(ближайший общий родитель)Panel
Panel
В данном примере это компонент Accordion
. Поскольку он находится выше обеих панелей и может контролировать их пропсы, он станет “источником истины”, чтобы определить, какая панель сейчас активна. Давайте сделаем так, чтобы Accordion
передавал значения isActive
хардкодом (например, true
) обеим панелям:
import { useState } from 'react'; export default function Accordion() { return ( <> <h2>Алматы, Казахстан</h2> <Panel title="Подробнее" isActive={true}> Алматы с населением около 2 миллионов человек является крупнейшим городом Казахстана. С 1929 по 1997 год этот город был столицей. </Panel> <Panel title="Этимология" isActive={true}> Название происходит от <span lang="kk-KZ">алма</span>, казахского слова, означающего "яблоко", и часто переводится как "полное яблок". На самом деле, регион, прилегающий к Алматы, считается прародиной яблони, а дикий <i lang="la">Malus sieversii</i> считается вероятным кандидатом на роль предка современного домашнего яблока. </Panel> </> ); } function Panel({ title, children, isActive }) { return ( <section className="panel"> <h3>{title}</h3> {isActive ? ( <p>{children}</p> ) : ( <button onClick={() => setIsActive(true)}> Показать </button> )} </section> ); }
Попробуйте изменить хардкод значение isActive
в компоненте Accordion
и обратите внимание как это повлияет на результат.
Шаг 3: Добавить состояние в общего родителя
Подъём состояния часто приводит к изменению сущности хранимого состояния.
В нашем случае, только одна панель должна быть активна одновременно. Это означает, что общему родителю Accordion
нужно следить за тем какая панель активна в данный момент. Вместо булевого значения в состоянии можно хранить число, которое будет означать индекс активной панели:
const [activeIndex, setActiveIndex] = useState(0);
Когда activeIndex
равен 0
, активна первая панель, а когда равен 1
, активна вторая.
Нажатие на кнопку “Показать” в любой из панелей должно изменять индекс активной панели в Accordion
. Panel
не может напрямую установить состояние activeIndex
, потому что оно определено внутри Accordion
. Компоненту Accordion
необходимо явно разрешить компоненту Panel
изменять свое состояние, передав обработчик события как проп:
<>
<Panel
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
...
</Panel>
<Panel
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
...
</Panel>
</>
Элемент <button>
внутри компонента Panel
теперь будет использовать проп onShow
в качестве обработчика события click
:
import { useState } from 'react'; export default function Accordion() { const [activeIndex, setActiveIndex] = useState(0); return ( <> <h2>Алматы, Казахстан</h2> <Panel title="Подробнее" isActive={activeIndex === 0} onShow={() => setActiveIndex(0)} > Алматы с населением около 2 миллионов человек является крупнейшим городом Казахстана. С 1929 по 1997 год этот город был столицей. </Panel> <Panel title="Этимология" isActive={activeIndex === 1} onShow={() => setActiveIndex(1)} > Название происходит от <span lang="kk-KZ">алма</span>, казахского слова, означающего "яблоко", и часто переводится как "полное яблок". На самом деле, регион, прилегающий к Алматы, считается прародиной яблони, а дикий <i lang="la">Malus sieversii</i> считается вероятным кандидатом на роль предка современного домашнего яблока. </Panel> </> ); } function Panel({ title, children, isActive, onShow }) { return ( <section className="panel"> <h3>{title}</h3> {isActive ? ( <p>{children}</p> ) : ( <button onClick={onShow}> Показать </button> )} </section> ); }
Подъём состояния завершен! Переместив состояние в общий родительский компонент нам удалось скоординировать две панели. Использование индекса активной панели вместо двух флагов isActive
гарантирует нам, что будет активна только одна панель одновременно. А передав обработчики событий дочерним компонентам мы позволили им управлять состоянием родителя.
Deep Dive
Компоненты, у которых есть внутреннее состояние, обычно называют “неуправляемыми”. Например, оригинальный компонент Panel
с переменной состояния isActive
является неуправляемым, потому что его родитель не может повлиять на то, активна ли панель.
Напротив, компонент называют “управляемым”, если важная информация зависит от пропов, а не от внутреннего состояния. Это позволяет родительскому компоненту полностью контролировать его поведение. После наших изменений, компонентом Panel
управляет компонент Accordion
с помощью пропа isActive
.
Неуправляемые компоненты проще использовать, потому что им необходимо меньше конфигурации. Но они не такие гибкие, когда вам нужно согласовать их поведение. Управляемые компоненты максимально гибкие, но необходимо чтобы родительские компоненты полностью настраивали их через пропсы.
На практике, “управляемые” и “неуправляемые” это не строгие технические определения—обычно компоненты имеют и внутреннее состояние и пропсы. Однако, важно понимать как устроены компоненты и какие возможности они предоставляют.
Когда пишите компонент, подумайте какая информация в нем должна быть управляема (через пропсы), а какая неуправляема (через состояние). Но вы в любой момент можете передумать и сделать рефакторинг позже.
Единый источник истины для каждого состояния
В приложении на React у многих компонентов будет свое собственное состояние. Какое-то состояние может “жить” близко к листовым компонентам (компоненты внизу дерева), например, текстовое поле. Другое состояние может “жить” ближе к корню приложения. Например, даже при реализации библиотеки клиентской маршрутизации, текущий путь обычно сохраняют в состояние React и передают его вниз через пропсы.
Для каждого уникального кусочка состояния, вы будете выбирать, какой компонент будет им “владеть”. Этот принцип также известен как наличие “единого источника истины”. Это не означает, что все состояние React находится в одном месте—для каждой части состояния существует конкретный компонент, который хранит эту информацию. Вместо дублирования общего состояния между компонентами, поднимите его в общий родительский компонент и “пробросьте” его дочерним компонентам, которым он необходим.
Ваше приложение будет меняться по мере работы над ним. Это нормально, что вы будете перемещать состояние вниз или обратно вверх, пока разбираетесь где “живет” каждая часть состояния. Это все часть процесса!
Чтобы увидеть, как это работает на практике с другими компонентами, прочитайте статью Мышление в React..
Recap
- Если вы хотите скоординировать два компонента, переместите их состояние к общему родительскому компоненту.
- Затем передайте информацию через пропсы из общего родительского компонента.
- Наконец, пробросьте обработчики событий, чтобы дочерние компоненты могли изменять состояние родительского компонента.
- Полезно рассматривать компоненты как “управляемые” (управляемые пропами) или “неуправляемые” (управляемые состоянием).
Challenge 1 of 2: Синхронизированные поля ввода
Эти два поля ввода независимы. Синхронизируйте их: редактирование одного поля должно обновлять другое поле с тем же текстом, и наоборот.
import { useState } from 'react'; export default function SyncedInputs() { return ( <> <Input label="Первое поле" /> <Input label="Второе поле" /> </> ); } function Input({ label }) { const [text, setText] = useState(''); function handleChange(e) { setText(e.target.value); } return ( <label> {label} {' '} <input value={text} onChange={handleChange} /> </label> ); }