Вопрос «какую библиотеку для состояния взять» задают в начале проекта, когда состояния ещё нет. Отвечать на него в этот момент нечем, и именно поэтому ответ обычно оказывается неверным.
Тупик: выбрать библиотеку заранее
Первое, что пробуют, — выбрать хранилище на старте, вместе с остальным набором инструментов. Логика понятная: потом будет некогда, а переезжать дорого.
Это не помогает по простой причине: библиотеки состояния решают разные задачи, и какая из задач у вас появится, на старте неизвестно. Через полгода выясняется, что взяли инструмент для одного вида состояния, а болит другой — и живёте вы теперь с двумя, потому что переезжать действительно дорого.
Второй частый ход — не брать ничего и держать всё в компонентах, передавая вниз через свойства. Работает, пока дерево неглубокое. Дальше появляется передача через пять уровней ради одного значения в конце, и её начинают чинить контекстом, а контекст перерисовывает всё поддерево при каждом изменении. Получается медленно, и вину списывают на фреймворк.
Четыре вида состояния, а не одно
Слово «состояние» покрывает вещи, у которых мало общего. Разделив их, вы обычно обнаруживаете, что библиотека нужна не для всего, а для одной части — или не нужна вовсе.
Серверные данные. То, что пришло с сервера: списки, карточки, справочники. У них есть свойства, которых нет у остального: они устаревают, их надо перезапрашивать, они могут прийти с ошибкой, и один и тот же набор нужен в разных местах. Это не состояние приложения, это кеш чужих данных, и относиться к нему надо как к кешу.
Состояние интерфейса. Открыт ли выпадающий список, какая вкладка активна, что введено в поле. Живёт недолго, принадлежит одному месту, никому больше не нужно. Почти всегда должно оставаться в компоненте.
Состояние адреса. Фильтры, страница, сортировка, выбранный элемент. Его особенность в том, что оно должно переживать перезагрузку и попадать в ссылку, которой делятся. Место для него — адресная строка, а не хранилище: любое другое решение ломает ссылку.
Общее состояние приложения. Кто вошёл, какая тема, содержимое корзины. Немного по объёму, нужно во многих местах, меняется редко. Вот это и есть та часть, ради которой берут библиотеку.
Разложив состояние по четырём коробкам, легко увидеть главное: большая часть того, что собирались класть в хранилище, туда не относится.
Проверить разделение можно одним вопросом к каждому значению: что должно произойти при перезагрузке страницы. Серверные данные — запроситься заново. Состояние интерфейса — сброситься, и это правильно. Состояние адреса — восстановиться из ссылки. Общее состояние — восстановиться из хранилища или из сессии. Значение, для которого ответ неочевиден, почти всегда лежит не в той коробке.
Когда библиотека действительно нужна
Признак не «проект большой», а более узкий: одно и то же значение нужно нескольким удалённым друг от друга частям дерева, меняется в одном месте и читается в других, и передача через свойства уже превратилась в передачу ради передачи.
Второй признак — производительность. Если общее значение лежит в контексте и его изменение перерисовывает половину приложения, библиотека с точечными подписками решает ровно эту задачу. Но заметьте: это следствие измерения, а не предположения.
Третий — необходимость обращаться к состоянию вне отрисовки: из обработчика, из перехватчика запросов, из кода, который не является компонентом.
Если ни одного признака нет, библиотека добавит слоя абстракции и ничего не решит.
Стоит назвать и то, что признаком не является. Размер команды: библиотека не заменяет договорённостей о том, где что лежит, и в большой команде без таких договорённостей хранилище превращается в свалку быстрее, чем в маленькой. Количество экранов: двадцать независимых экранов, не делящих между собой ничего, прекрасно живут без общего состояния. И привычка: «мы всегда так делали» — довод про людей, а не про задачу, и его стоит называть своим именем, когда он звучит.
Что выбрать, когда нужна
Сравнивать стоит не по названиям, а по трём вопросам.
Как происходит подписка: перерисовывается ли компонент при любом изменении хранилища или только при изменении того, что он читает. Для приложения с частыми обновлениями это определяющий вопрос.
Как устроено обновление: меняется ли состояние напрямую или через описание изменения. Первое короче в записи, второе даёт историю изменений и упрощает разбор ошибок в сложных случаях.
Сколько кода нужно, чтобы добавить одно новое значение. Эту величину умножают на сотни, и она определяет, будут ли инструментом пользоваться так, как задумано, или начнут обходить.
Четвёртый вопрос обычно вспоминают поздно: как это отлаживается. Возможность увидеть текущее содержимое и историю изменений в инструментах разработчика экономит часы в тот момент, когда значение на экране не соответствует ожиданиям, а причина где-то в цепочке обновлений.
Отдельно: серверные данные почти всегда стоит держать не в общем хранилище, а в инструменте, предназначенном для них. Он умеет то, чего от хранилища не ждут, — повторные запросы, устаревание, объединение одинаковых обращений, обработку ошибок.
Как понять, что ошиблись
Признаков два, и оба видны рано. Первый: в хранилище лежат данные, пришедшие с сервера, и рядом руками написан код их обновления, инвалидации и обработки ошибок — вы пишете кеш вместо того, чтобы взять готовый.
Второй: чтобы понять, откуда взялось значение на экране, приходится идти по цепочке из нескольких файлов. Хранилище задумывалось как способ упростить, а стало способом спрятать.
В обоих случаях лечение — не смена библиотеки, а возврат части состояния туда, где ему место: в компонент, в адрес, в инструмент для серверных данных.
Смена библиотеки в такой ситуации выглядит как решение и обходится дорого: переносится всё, включая то, что переносить не следовало, и через год история повторяется с новым названием на обложке.
Позиция редакции
Мы считаем, что решение о библиотеке принимается не на старте, а тогда, когда появился первый признак из перечисленных. До этого момента хватает состояния в компонентах, адресной строки и инструмента для серверных данных — и это не временное решение, а нормальное устройство большинства приложений.
Мы неправы, если приложение с самого начала строится вокруг сложного клиентского состояния, которое живёт долго и меняется часто: редактор, таблица с расчётами, интерфейс реального времени. Там хранилище — часть архитектуры, и откладывать выбор не стоит.