State Management

Valtio: proxy-состояние

В редакторе диаграмм вроде Excalidraw на холсте сотни фигур, у каждой координаты, цвет, поворот, вложенные группы. Через Redux пришлось бы для сдвига одной фигуры на пиксель собрать новый массив, новый объект фигуры, новый объект группы вверх по дереву. Через Zustand похожая история с set и поверхностным слиянием. Valtio задаёт другой вопрос: а что, если просто написать shape.x += 1, как с обычным объектом, и React сам увидит изменение и перерисует только тех, кто читал именно x.

  • Канвас-редакторы и доски: Excalidraw-подобные сцены, где у каждой фигуры вложенные свойства
  • Scene graph в 3D и 2D: react-three-fiber-сцены, где состояние объектов глубоко вложено
  • Конструкторы интерфейсов и no-code билдеры с деревом компонентов
  • Сложные формы-мастера, где удобнее мутировать draft напрямую, а не собирать новый объект
  • Игровое состояние в браузере: инвентарь, позиции сущностей, таймеры в одном вложенном сторе

Предварительные знания

  • Нативный Proxy и перехват операций get и set
  • Разница между мутацией объекта и созданием нового иммутабельного значения
  • Почему React сравнивает ссылки и поэтому требует новый объект для ре-рендера
  • Observable через Proxy

Откуда взялся proxy-стор

Valtio появился в 2020 году в коллективе Poimandres, том же, что выпустил Zustand и Jotai. Автор, Дайси Като, искал модель, где состояние можно менять как обычный мутируемый объект, но React при этом получал бы стабильные иммутабельные срезы. Решение опирается на два механизма браузера: Proxy перехватывает мутации и помечает изменившиеся ветки, а useSnapshot отдаёт компоненту замороженный снимок, отслеживая какие именно поля читались при рендере. Так мутабельный код для разработчика сочетается с иммутабельным контрактом для React.

proxy() и прямая мутация

Стор в Valtio создаётся функцией proxy, которой передаётся обычный объект. Возвращается реактивная обёртка с тем же набором полей. Изменение состояния это прямое присваивание: state.count += 1 или state.user.name = 'Аня'. Никакого set, никакого dispatch, никакой сборки нового объекта. Под капотом нативный Proxy перехватывает запись и помечает изменившуюся ветку как устаревшую.

Стор это модуль-синглтон. Менять его можно откуда угодно: из обработчика события, из таймера, из обычной функции вне React. Прокси отслеживает мутации одинаково, независимо от того, кто их вызвал. Это роднит Valtio с другими сторами без Provider: дерево оборачивать не нужно.

Действия в Valtio это просто функции, которые мутируют стор. Они могут лежать рядом со стором или в отдельном модуле. Прокси перехватывает и push в массив, и изменение length, поэтому привычные методы массива работают без обёрток.

Как в Valtio меняется состояние стора, созданного через proxy()?

useSnapshot() и точечные ре-рендеры

В компоненте стор читается не напрямую, а через useSnapshot. Хук возвращает иммутабельный замороженный срез состояния на момент рендера. Пока компонент рисуется, useSnapshot отслеживает, какие именно поля среза были прочитаны, и подписывает компонент только на них. Изменение полей, которых компонент не касался, ре-рендер не вызывает.

Здесь видно разделение ролей. Чтение идёт через snap: это замороженный объект, его поля менять нельзя. Запись идёт через сам state: прямая мутация. Компонент Counter прочитал только snap.count, поэтому изменение state.theme его не тронет. Это то же точечное обновление, что дают селекторы в Zustand, но без явного селектора: подписка выводится из того, что было прочитано.

Менять поля снимка нельзя: snap заморожен через Object.freeze. Попытка snap.count = 5 ни к чему не приведёт, а в строгом режиме бросит ошибку. Запись всегда идёт в исходный state, чтение всегда из snap. Смешение этих ролей частый источник путаницы у новичков в Valtio.

Отслеживание чтения работает на любой глубине. Компонент, прочитавший snap.canvas.zoom, не перерисуется при сдвиге фигуры в shapes. Не нужно вручную писать селектор пути: достаточно прочитать нужное поле из снимка, и подписка настроится сама.

Зачем в компоненте читать состояние через useSnapshot(), а не напрямую из proxy-стора?

Почему proxy-модель удобна для вложенного UI

Сильная сторона Valtio проявляется там, где состояние глубоко вложено и часто мутирует точечно. Классический пример это канвас-редактор: дерево фигур, у каждой координаты, размер, поворот, вложенные группы. В иммутабельной модели сдвиг одной фигуры требует пересобрать массив фигур и все объекты-родители вверх по дереву. В Valtio это одна строка прямой мутации, а прокси сам помечает только изменившуюся ветку.

  • Иммутабельный подход — Сдвиг фигуры требует новый объект фигуры, новый массив shapes и новый объект сцены вверх по дереву. Много кода и шанс забыть скопировать уровень
  • Proxy-подход Valtio — shape.x += dx меняет ровно нужное поле. Прокси помечает только эту ветку, snapshot пересобирается лишь там, где затронуто

Каждый ShapeView читает только свою фигуру из снимка, поэтому сдвиг фигуры A не перерисовывает фигуру B. Для scene graph в react-three-fiber и no-code билдеров это естественная модель: дерево объектов мутируется как обычная структура, а перерисовка остаётся точечной. Там, где состояние плоское и редко вложенное, выигрыш proxy-модели меньше, и Zustand с явными селекторами часто читается понятнее.

Граница применимости честная. Valtio силён на глубоко вложенном, часто мутирующем состоянии: канвас, сцены, конструкторы. На простом плоском UI-состоянии вроде одной модалки или темы разница с Zustand или Jotai мала, и выбор уходит в сторону вкуса команды и привычной ментальной модели.

Для какого состояния proxy-модель Valtio даёт наибольшее преимущество над иммутабельным подходом?

Связь с другими темами

Этот урок открывает группу лёгких сторов. Рядом стоят соседи по подходу:

  • Observable через Proxy — Valtio это прикладная надстройка над тем же Proxy, что разбирается в базовом уроке про реактивность
  • Лёгкие сторы: сравнение — Valtio сравнивается с Zustand, Jotai и Nano Stores по ментальной модели и размеру
  • Zustand — Соседний стор того же коллектива, но с явным set вместо прямой мутации

Итог

  • proxy() заворачивает обычный объект в реактивную обёртку, и мутации делаются напрямую: state.x += 1
  • useSnapshot() возвращает иммутабельный замороженный срез и подписывает компонент только на прочитанные поля
  • Мутабельный код снаружи сочетается с иммутабельным контрактом для React: ре-рендер точечный
  • Глубокая вложенность работает из коробки: мутация state.canvas.shapes[3].x перехватывается прокси
  • Модель удобна для канвас-редакторов, scene graph и no-code билдеров, где состояние глубоко вложено
  • Valtio из коллектива Poimandres, как Zustand и Jotai, но опирается на прямую мутацию вместо set или атомов

Связанные уроки

  • sm-08-observable-proxy — Valtio построен на нативном Proxy, поэтому понимание перехвата get и set обязательно
  • sm-21-lightweight-comparison — Valtio встаёт в один ряд с Zustand, Jotai и Nano Stores, и сравнение покажет где proxy-модель выигрывает
  • rc-38-zustand-state — Тот же коллектив Poimandres, что сделал Zustand, выпустил и Valtio, но с другой ментальной моделью
Valtio: proxy-состояние

0

1

Войти