useControllableState

Хук useControllableState из библиотеки Chakra UI используется для создания компонентов, которые могут работать одновременно в двух режимах управления состоянием:

  • контролируемом (controlled)
  • неконтролируемом (uncontrolled)

Это распространённый паттерн проектирования UI-компонентов в экосистеме React. Он позволяет компоненту либо управлять своим состоянием самостоятельно, либо принимать значение состояния извне через props.

В Chakra UI этот хук используется внутри многих компонентов библиотеки: например, в элементах управления вроде Switch, Tabs, Accordion, Slider и других интерактивных компонентов.

Основная задача useControllableStateобъединить controlled и uncontrolled поведение в одном API, избавляя разработчика от необходимости вручную писать однотипную логику синхронизации.


Controlled и Uncontrolled состояние

В React существуют два способа управления состоянием компонента.

Контролируемое состояние

Контролируемый компонент получает значение через props и сообщает об изменениях через callback.

const [value, setValue] = useState("A")

<Tabs value={value} onCha nge={setValue} />

Особенности:

  • состояние хранится в родительском компоненте
  • компонент становится предсказуемым
  • вся логика управления находится снаружи

Неконтролируемое состояние

Компонент управляет состоянием внутри себя, используя начальное значение.

<Tabs defaultValue="A" />

Особенности:

  • состояние хранится внутри компонента
  • родитель не управляет изменениями напрямую
  • используется defaultValue

Проблема при разработке библиотечных компонентов

При создании UI-библиотек часто требуется поддерживать оба режима одновременно.

Пример API:

<Slider value={50} onCha nge={setValue} />

или

<Slider defaultValue={50} />

Без специальных инструментов разработчик должен самостоятельно писать проверку:

  • передан ли value
  • используется ли defaultValue
  • нужно ли обновлять внутреннее состояние
  • нужно ли вызывать onChange

Эта логика быстро становится громоздкой и повторяется во многих компонентах.

useControllableState решает эту проблему.


Базовая сигнатура useControllableState

const [value, setValue] = useControllableState(options)

Параметры передаются через объект конфигурации.

Основные параметры:

параметр описание
value контролируемое значение
defaultValue начальное значение для uncontrolled режима
onChange обработчик изменения состояния
shouldUpdate функция проверки необходимости обновления

Простейший пример использования

import { useControllableState } from "@chakra-ui/react"

function Counter(props) {
  const [count, setCount] = useControllableState({
    value: props.value,
    defaultValue: 0,
    onChange: props.onChange,
  })

  return (
    <button onCl ick={() => setCount(count + 1)}>
      {count}
    </button>
  )
}

Теперь компонент поддерживает два режима.

Контролируемый:

const [value, setValue] = useState(10)

<Counter value={value} onCha nge={setValue} />

Неконтролируемый:

<Counter defaultValue={10} />

Логика работы хука

Внутри useControllableState реализована следующая стратегия.

  1. Проверяется, передан ли value.

  2. Если value существует — компонент считается controlled.

  3. Если value отсутствует — используется внутренний state.

  4. При изменении состояния:

    • вызывается onChange
    • обновляется внутренний state (только если компонент uncontrolled)

Схема:

value !== undefined
        │
        ├── controlled → используется props.value
        │
        └── uncontrolled → используется useState(defaultValue)

Детальный пример компонента

Рассмотрим упрощённую реализацию переключателя.

function Toggle(props) {
  const [isOn, setIsOn] = useControllableState({
    value: props.isOn,
    defaultValue: false,
    onChange: props.onChange
  })

  const toggle = () => {
    setIsOn(!isOn)
  }

  return (
    <button onCl ick={toggle}>
      {isOn ? "ON" : "OFF"}
    </button>
  )
}

Использование в controlled режиме:

function App() {
  const [on, setOn] = useState(false)

  return (
    <Toggle
      isOn={on}
      onCha nge={setOn}
    />
  )
}

Uncontrolled режим:

<Toggle defaultValue={true} />

Параметр shouldUpdate

Иногда необходимо предотвратить обновление состояния при определённых условиях.

Для этого используется параметр shouldUpdate.

const [value, setValue] = useControllableState({
  value: props.value,
  defaultValue: 0,
  onChange: props.onChange,
  shouldUpdate: (prev, next) => prev !== next
})

Функция получает:

  • prev — предыдущее значение
  • next — новое значение

Если функция возвращает false, обновление не происходит.


Сравнение с useState

useControllableState можно рассматривать как расширение useState, которое добавляет поддержку controlled компонентов.

Обычный useState:

const [value, setValue] = useState(0)

useControllableState:

const [value, setValue] = useControllableState({
  value,
  defaultValue: 0,
  onChange
})

Главное отличие:

характеристика useState useControllableState
controlled режим нет да
uncontrolled режим да да
синхронизация props нет да
вызов onChange вручную автоматически

Поведение setValue

Функция setValue работает аналогично setState:

setValue(newValue)

или

setValue(prev => prev + 1)

Однако при controlled режиме:

  • внутренний state не обновляется
  • вызывается только onChange

Таким образом источник истины остаётся у родителя.


Поддержка функционального обновления

useControllableState поддерживает обновление через функцию.

setValue(prev => prev + 1)

Это особенно важно при асинхронных обновлениях и при работе с несколькими вызовами состояния.


Сценарии применения

Хук активно применяется при разработке:

1. Интерактивных контролов

  • переключатели
  • слайдеры
  • вкладки
  • аккордеоны

2. Полей ввода

Компоненты формы часто должны поддерживать controlled и uncontrolled режим.


3. Компонентов состояния UI

Например:

  • раскрывающиеся панели
  • модальные окна
  • меню
  • всплывающие элементы

Пример компонента аккордеона

function AccordionItem(props) {
  const [isOpen, setIsOpen] = useControllableState({
    value: props.isOpen,
    defaultValue: false,
    onChange: props.onChange
  })

  return (
    <div>
      <button onCl ick={() => setIsOpen(!isOpen)}>
        Toggle
      </button>

      {isOpen && <div>{props.children}</div>}
    </div>
  )
}

Использование controlled режима:

<AccordionItem
  isOpen={open}
  onCha nge={setOpen}
/>

Обработка defaultValue

defaultValue используется только при первом рендере.

Если компонент uncontrolled, значение сохраняется во внутреннем состоянии.

Изменение defaultValue после монтирования не влияет на state.

Это поведение полностью соответствует useState.


Типичная ошибка при использовании

Распространённая ошибка — одновременное использование value и defaultValue.

<MyComponent
  value={10}
  defaultValue={5}
/>

В этом случае:

  • value имеет приоритет
  • defaultValue игнорируется

Компонент становится controlled.


Паттерн проектирования API компонентов

useControllableState помогает формировать стандартный API.

Типичный интерфейс компонента:

value
defaultValue
onChange

Этот паттерн используется во многих библиотеках:

  • Chakra UI
  • Radix UI
  • Material UI
  • Ant Design

Он делает компоненты предсказуемыми и гибкими.


Упрощённая внутренняя реализация

Пример приближённой реализации:

function useControllableState({
  value,
  defaultValue,
  onChange
}) {
  const [state, setState] = useState(defaultValue)

  const isControlled = value !== undefined

  const currentValue = isControlled ? value : state

  const setValue = (next) => {
    const nextValue =
      typeof next === "function"
        ? next(currentValue)
        : next

    if (!isControlled) {
      setState(nextValue)
    }

    onChange?.(nextValue)
  }

  return [currentValue, setValue]
}

Настоящая реализация Chakra UI содержит дополнительные оптимизации:

  • сравнение значений
  • предотвращение лишних обновлений
  • стабильные ссылки функций

Преимущества использования

Универсальность

Один компонент работает в двух режимах.


Чистый API

Пользователь компонента получает знакомый интерфейс.


Повторное использование

Логика controlled/uncontrolled не дублируется.


Предсказуемость

Поведение компонентов соответствует стандартам React.


Когда использовать useControllableState

Хук оправдан при разработке:

  • библиотек компонентов
  • переиспользуемых UI-элементов
  • компонентов со сложной логикой состояния

Для обычных компонентов приложения чаще достаточно useState.


Связанные хуки Chakra UI

useControllableState часто используется вместе с другими утилитами Chakra:

  • useDisclosure
  • useBoolean
  • useCallbackRef
  • useMergeRefs

Они формируют инфраструктуру управления состоянием в компонентах Chakra UI.


Архитектурная роль в Chakra UI

useControllableState является частью низкоуровневого слоя управления состоянием Chakra UI.

Он обеспечивает:

  • единообразие API компонентов
  • поддержку controlled/uncontrolled паттернов
  • минимизацию дублирования логики

Через этот хук построено большое количество компонентов библиотеки, что делает его одним из ключевых инструментов при разработке сложных UI-элементов на основе Chakra UI.