Статья является расшифровкой части доклада.
И так начнем с официального описания:
Zustand - не большое, быстрое и масштабируемое решение для управления состоянием, основанное на принципах Flux и immutable state. Имеет удобный API, основанный на хуках, не создает лишнего шаблонного кода и не навязывает жестких правил использования. Не имеет проблем с Zombie children и context loss и отлично работает в React concurrency mode.
Zustand работает в любой окружении:
в коде React
в ванильном JavaScript
Его архитектура базируется на publish/subscribe объекте и реализации единственного хука для React.
Легковесный

Тут даже нечего добавить - 693 байта! Не знаю можно ли найти реализацию еще меньше чем эта.
Простой
Zustand имеет простой синтаксис создания хранилища:
const storeHook = create((set, get) => { ... store config ... });
Для примера создадим простое хранилище:
import { create } from 'zustand'; export const useCounterStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), }));
В результате создания хранилища мы получаем React хук, который используем для реактивного отображения изменений данных в компонентах.
Хранилище можно создавать и изменять как в контексте приложения React так и в javascript вне контекста React (не каждый стейт-менеджер способен на такое).
Zustand для обнаружения изменений использует иммутабельность - привычные методы работы с данными в React.
В Zustand вы можете создавать столько хранилищ, сколько вашей душе будет угодно.
import { create } from 'zustand'; export const useCatsStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), })); export const useDogsStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), }));
Созданный хук имеет простой синтаксис
storeHook: (selector) => selectedState;
где selector - это простая функция для извлечению данных из переданного ей состояния хранилища
const stateData = storeHook((state) => { // извлечение данных из state ... return selectedState; });
Созданный React хук, легко использовать в компонентах
function Counter() { const { count, increment } = useCounterStore((state) => state); return <button onClick={increment}>{count}</button>; }
Удобный
Хук для удобства имеет статические методы (методы объекта pub/sub) :
{ getState: () => state, setState: (newState) => void, subscribe: (callback) => void, ... }
Статические методы очень удобно использовать в обработчиках
function Counter() { const count = useCounterStore((state) => state.counter); function onClick() { const state = useCounterStore.getState(); useCounterStore.setState({ count: state.count + 3; }); } return <button onClick={onClick}>{count}</button>; }
В хранилище можно использовать асинхронные вызовы:
const useCounterStore = create((set) => ({ count: 0, increment: async () => { const resp = await fetch('https://my-bank/api/getBalance'); const { balance } = await resp.json(); set((state) => { return { count: state.count + balance }; }); }, }));
Асинхронные вызовы так же можно выполнять и в компоненте
function Counter() { const count = useCounterStore((state) => state.counter); function onClick() { const resp = await fetch('https://my-bank/api/getBalance'); const { balance } = await resp.json(); useCounterStore.setState({ count: count + balance, }); } return <button onClick={onClick}>{count}</button>; }
Методы можно создавать и вне хранилища
const useCounterStore = create(() => ({ count: 0, })); const increment = () => { const state = useCounterStore.getState(); useCounterStore.setState({ count: state.count + 1 }); };
В Zustand как и в Redux можно создавать свои middleware!
С ним поставляются готовые middlewares:
persist: для сохранения/восстановления данных в/из localStorage
immer: для простого мутабельного изменения состояния
devtools: для работы с расширением redux devtools для отладки
Производительный
А чтоже под капотом ?
Привожу наивную реализацию хука Zustand, который отображает его логику
function useStore(selector) { const [prevState, setPrevState] = useState(store.getState(selector)); useEffect(() => { const unsubscribe = store.subscribe((newState) => { const currentState = selector(newState); if (currentState !== prevState) { setPrevState(currentState); } }); return () => { unsubscribe(); }; }, [selector]); return prevState; }
В момент инициализации хука из хранилища выбирается текущее состояние для селектора и записывается в состояние хука.
Далее в эффекте производится подписка на изменения pusb/sub объекта хранилища, в которой, при возникновении обновлений данных хранилища, производится выборка данных при помощи селектора. Затем производится сравнение ссылок текущих данных со ссылкой на данные, сохраненные ранее в состоянии хука. Если ссылки не равны ( а мы помним, что при иммутабельном изменении данных в объекте меняются ссылки на данные) - вызывается метод обновления состояния хука.
Куда уж проще и эффективнее !?
Сравнение ссылок это простая и очень эффективная операция
Так как данные хранилища хранятся вне контекста React ( во внешнем объекте pub/sub ) данный хук будет пропускать изменения состояний хранилища между рендерами в Concurrency mode в React 18+, поэтому реальная реализация хука Zustand базируется на новом хуке React для синхронной перерисовки дерева компонент
function useStore(store, selector, equalityFn) { return useSyncExternalStoreWithSelector( store.subscribe, store.getState, store.getServerState || store.getInitialState, selector, equalityFn, ); }
В Concurrency mode в случае изменения данных в хранилище React будет прерывать параллельное построение дерева DOM и будет запускать синхронный рендер для того, чтобы отобразить новые изменения данных в хранилище - это гарантирует что ни одно изменение в хранилище не будет пропущено и будет отрисовано.
Команда React работает над тем, чтобы в будущем избежать грубого прерывания параллельной работы React и выполнять своевременную отрисовку изменений в контексте конкурентного режима.
Методы оптимизации
Методы оптимизации вытекают из выше приведенного кода реализации хука - хук useEffect напрямую зависит от селектора, поэтому мы должны обеспечить постоянство ссылки на сам селектор и на данные возвращаемые им!
Их всего 3 простых метода оптимизации
если селектор не зависит от внутренних переменных - вынесите его за пределы компонента, таким образом селектор не будет создаваться каждый раз и ссылка на него будет постоянной
const countSelector = (state) => state.count; function Counter() { const count = useCounterStore(countSelector); return <div>{count}</div>; }
если селектор имеет параметры - заключите его в useCallback - таким образом мы обеспечим постоянство ссылки на селектор в зависимости от значения параметра
function TodoItem({ id }) { const selectTodo = useCallback((state) => state.todos[id], [id]); const todo = useTodoStore(selectTodo); return ( <li> <span>{todo.id}</span> <span>{todo.text}</span> </li> ); }
если селектор возвращает каждый раз новый объект - оберните вызов селектора в useShallow - если в новом объекте сами данные реально не изменились - хук useShallow выдаст ссылку на ранее сохраненный объект
import { useShallow } from 'zustand/react/shallow'; const selector = (state) => Object.keys(state); function StoreInfo() { const entityNames = useSomeStore(useShallow(selector)); return ( <div> <div>{entityNames.join(', ')}</div> </div> ); }
если же селектор имеет и параметры и возвращает новый объект - оберните его в useCallback и затем в useShallow
import { useShallow } from 'zustand/react/shallow'; function TodoItemFieldNames({ id }) { const selectFieldNames = useCallback((state) => Object.keys(state.todos[id]), [id]); const fieldNames = useTodoStore(useShallow(selectFieldNames)); return ( <li> <span>{fieldNames.join(', ')}</span> </li> ); }
Есть еще один "турбо" способ оптимизации отображения изменения данных в хранилище минуя цикл рендера React - использовать подписку на изменения данных и манипулировать элементами DOM напрямую
function Counter() { const counterRef = useRef(null); const countRef = useRef(useCounterStore.getState().count); useEffect( () => useCounterStore.subscribe((state) => { countRef.current = state.count; counterRef.current.textContent = countRef.current; }), [], ); return ( <div> <span>Count: </span> <span ref={counterRef}>{countRef.current}</span> </div> ); }
При инициализации компонента запоминаем актуальное состояние в мутабельном объекте countRef и при перерисовках компонента отображаем его. В случае срабатывания подписки - получаем новое состояние, записываем его в мутабельный объект countRef и затем изменяем текстовое содержимое ссылки на DOM элемент counterRef.
В случае если затем React по какой-то причине будет перерисовывать наш компонент - у нас всегда будет актуальное состояние хранилища в мутабельном объекте countRef.
Лучшие практики
Все связанные сущности держите в одном хранилище
Zustand позволяет создавать не ограниченное количество хранилищ, но на практике отдельное хранилище нужно создавать только для данных, которые реально не связаны с данными в других хранилищах иначе вы вернетесь в прошлое - известная проблема model hell из MVC и FLUX (большое количество моделей имеющих хаотические связи с не прозрачной логикой взаимодействия с разнонаправлен��ыми потоками данных, как правило, всегда ведущих к зацикливанию обновлений и очень долгими отладками по вечерам для обнаружения этих зацикливаний - более подробно в докладе).
В настоящее время молодое поколение разработчиков, не работавших с MVC и FLUX и не изучавших теорию, создающих различные атомные, молекулярные, протонные, нейтронные и всякие кварковые стейт-менеджеры, наступают на грабли, которые индустрия прошла много лет назад.
Связанные структуры данных должны хранится в одном хранилище.
Создание отдельных хранилищ оправдано лишь для реально не зависимых структур данных.
Методы хранилища создавайте вне его описания
Не смотря на то что Zustand позволяет создавать методы хранилища прямо в нем самом, рекомендую не создавать методы хранилища внутри него - если у вас в хранилище много сущностей или сущности сложные, а их методы обработки достаточно объемные - описание вашего хранилища станет не читаемым, не понимаемым и плохо поддерживаемым.
Размещение методов в отдельных модулях дает еще один бонус - кроме того что мы визуально изолируем код метода, что делает его легче для чтения и понимания, это позволяет легко тестировать метод в изоляции от самого хранилища.
Доступ к данным в хранилище только при помощи его методов!
Я много лет проектировал и сопровождал базы данных, и для меня не понятно почему в подавляющем большинстве проектов часть базы данных, представленная на клиенте хранилищем, являющегося частью бизнес логики данных, не обеспечивает эту бизнес логику!
Нет ни кода по обеспечению целостности данных, ни их непротиворечивости и безопасности!
Подавляющее количество хранилищ состояния приложения, которые я видел промышленной эксплуатации - это простые Json хранилки! Это не хранилище состояния, а именно простые хранилки json.
Если понять, что эти данные и есть то ядро приложения, вокруг которого строится приложение, то становится понятно, что как это хранилище реализовано - таким и будет надежность приложения. Если хранилище (ядро приложения) - это простая Json хранилка - то 99.99% что приложение будет лихорадить от большого количества багов. Так или иначе код по валидации данных будет размазан и дублирован по разным частям приложения. А отсутствие обеспечения целостности данных и их непротиворечивости гарантированно будет приводить к багам которые будут возникать постоянно в большой команде.
Чтобы создать надежное хранилище и тем самым ликвидировать большинство багов в приложении:
разработчик не должен иметь прямой доступ к внутренностям хранилища
данные должны извлекаться и обрабатываться исключительно при помощи методов хранилища
методы хранилища должны обеспечивать целостность и непротиворечивость данных
import { create } from 'zustand'; // никода не экспортируем сам хук с его методами доступа к хранилищу! const useCounterStore = create(() => ({ count: 0, })); // экспортируем пользовательский хук - не даем доступ ко всему содержимому хранилища export const useCounter = () => useCounterStore((state) => state.count); // экспортируем метод export const increment = () => { const state = useCounterStore.getState(); useCounterStore.setState({ count: state.count + 1 }); };
Так же рекомендую производить полное клонирование данных на входе и на выходе из методов - ни один джун не сломает ваше хранилище путем мутирования данных из хранилища (для эксперимента попробуйте в возвращенных данных изменить что-либо - вы будете не приятно удивлены результатом - вы напрямую измените данные в хранилище).
Инкапсуляция бизнес-логики в методах хранилища так же позволит безболезненно менять стейт-менеджер в случае необходимости.
Заключение:
Если посмотреть на реализацию Zustand и оглянуться на 10 лет назад, когда у нас уже были активные модели типа pub/sub, и нам оставалось лишь объединить весь зоопарк этих моделей в одно хранилище и реализовать аналог хука (что элементарно реализуется в на старых классовых компонентах) можно понять, что в какой-то момент индустрия свернула не туда и мы пошли не за теми и лишь спустя 10 лет мы наконец-то вышли на правильную дорогу: однонаправленный поток данных (компонент -> реакция юзера -> изменение данных в хранилище -> хук для отображения изменений) и хранение связанных структур данных в одном хранилище.
По пути нам пришлось вначале изобрести FLUX, и затем уйти еще дальше в сторону от простого решения на Redux, параллельно на ощупь искать правильное решение при помощи тысяч реализаций стейт менеджеров.
Но хорошо то, что хорошо заканчивается!
Zustand - это просто мечта разработчика!
компактный
простой в использовании
не добавляющий когнитивной нагрузки
производительный
легко оптимизируемый
легко масштабируемый
имеющий легко поддерживаемый код
исключающий проблемы типа Zombie children и context loss
поддерживающий работу в React concurrency mode
Мы используем Zustand практически с момента его появления на свет и испытываем только положительные эмоции от его применения.
Начните активно использовать Zustand в своих проектах - вы будете приятно удивлены простотой и удобством работы с ним.
