Контроль состояния извне

В библиотеке Radix UI все компоненты построены с учётом управления состоянием как изнутри (internal state), так и извне (controlled state). Контроль состояния извне позволяет интегрировать Radix-компоненты в более сложные сценарии, где состояние управляется глобально или через сторонние менеджеры состояния, такие как Redux, Zustand или Recoil.


Controlled vs Uncontrolled

Uncontrolled (неуправляемый) компонент хранит своё состояние внутренне. Например, компонент Accordion сам управляет тем, какие панели открыты.

Controlled (управляемый) компонент получает состояние извне через пропсы и уведомляет об изменениях через callback-функции. Это позволяет полностью синхронизировать состояние компонента с внешними источниками данных.

Пример различий:

// Неуправляемый Accordion
<Accordion>
  <AccordionItem value="item-1">
    <AccordionTrigger>Панель 1</AccordionTrigger>
    <AccordionContent>Контент 1</AccordionContent>
  </AccordionItem>
</Accordion>

// Управляемый Accordion
const [openItem, setOpenItem] = useState<string | null>(null);

<Accordion value={openItem} onValueCha nge={setOpenItem}>
  <AccordionItem value="item-1">
    <AccordionTrigger>Панель 1</AccordionTrigger>
    <AccordionContent>Контент 1</AccordionContent>
  </AccordionItem>
</Accordion>

Ключевые моменты:

  • value задаёт текущее состояние компонента.
  • onValueChange сообщает о попытках изменения состояния.
  • В управляемом режиме компонент полностью зависит от внешнего состояния и не хранит внутреннего значения.

Примеры компонентов с управляемым состоянием

Accordion

Управляемый Accordion позволяет синхронизировать открытые панели с глобальным состоянием:

const [openItem, setOpenItem] = useState<string | null>(null);

<Accordion type="single" value={openItem} onValueCha nge={setOpenItem}>
  <AccordionItem value="item-1">
    <AccordionTrigger>Панель 1</AccordionTrigger>
    <AccordionContent>Контент панели 1</AccordionContent>
  </AccordionItem>
  <AccordionItem value="item-2">
    <AccordionTrigger>Панель 2</AccordionTrigger>
    <AccordionContent>Контент панели 2</AccordionContent>
  </AccordionItem>
</Accordion>
  • Проп type="single" определяет возможность открытия только одной панели одновременно.
  • value={openItem} контролирует, какая панель открыта.
  • onValueCha nge={setOpenItem} синхронизирует изменения с внешним состоянием.

Tabs

Компонент Tabs отлично демонстрирует управление состоянием извне:

const [selectedTab, setSelectedTab] = useState('tab1');

<Tabs value={selectedTab} onValueCha nge={setSelectedTab}>
  <TabsList>
    <TabsTrigger value="tab1">Вкладка 1</TabsTrigger>
    <TabsTrigger value="tab2">Вкладка 2</TabsTrigger>
  </TabsList>
  <TabsContent value="tab1">Контент вкладки 1</TabsContent>
  <TabsContent value="tab2">Контент вкладки 2</TabsContent>
</Tabs>
  • value задаёт активную вкладку.
  • onValueChange позволяет обрабатывать переключение и синхронизировать его с внешними состояниями или эффектами.

Передача состояния через props

Для всех управляемых компонентов Radix важно правильно использовать props, которые отвечают за состояние:

  1. value / defaultValue – текущее значение или начальное состояние.

    • value используется для контролируемого компонента.
    • defaultValue для неуправляемого, устанавливает начальное состояние.
  2. onValueChange / onOpenChange – callback для уведомления о попытке изменения состояния.

  3. disabled / readOnly – позволяют блокировать управление извне, сохраняя визуальное поведение.


Интеграция с глобальным состоянием

Использование управляемого режима открывает возможности интеграции с Redux, Zustand, Recoil или Context API. Пример с Zustand:

import create from 'zustand';

const useStore = create(set => ({
  openAccordion: null,
  setOpenAccordion: (value) => set({ openAccordion: value })
}));

function AccordionWrapper() {
  const { openAccordion, setOpenAccordion } = useStore();

  return (
    <Accordion value={openAccordion} onValueCha nge={setOpenAccordion}>
      <AccordionItem value="item-1">
        <AccordionTrigger>Панель 1</AccordionTrigger>
        <AccordionContent>Контент панели 1</AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-2">
        <AccordionTrigger>Панель 2</AccordionTrigger>
        <AccordionContent>Контент панели 2</AccordionContent>
      </AccordionItem>
    </Accordion>
  );
}
  • Вся логика состояния выносится за компонент, что повышает предсказуемость поведения.
  • Возможность подписки на изменения состояния из разных частей приложения.

Практические советы

  • Всегда использовать value и onValueChange для синхронизации компонента с внешним состоянием, если требуется согласованное управление.
  • Не смешивать defaultValue и value в одном компоненте — это создаёт непредсказуемое поведение.
  • Для сложных сценариев с несколькими зависимыми компонентами использовать управляемый режим обязательно, иначе внутреннее состояние может конфликтовать.
  • Использовать onValueChange для побочных эффектов, таких как запросы к API, а не только для обновления локального состояния.

Поддержка анимаций и переходов

Radix UI автоматически поддерживает анимацию раскрытия или переключения контента в управляемом режиме. Важно только управлять состоянием через value и onValueChange. Анимации не ломаются при контролируемом состоянии, так как компонент всегда знает текущее и предыдущее значение.


Итоговые ключевые моменты

  • Контролируемый режим делает компонент полностью зависимым от внешнего состояния.
  • Использование value и onValueChange позволяет интегрировать компоненты в глобальные хранилища состояния.
  • Управляемый режим необходим для сложных интерфейсов с синхронизацией между несколькими компонентами.
  • Правильное применение предотвращает конфликты состояния и упрощает предсказуемость поведения интерфейса.