Zustand.js - современный, невесомый, производительный и очень гибкий state manager
Tags: javascript, react, statemanagers, zustand
Published: 2024-03-12 17:06:40

---
title: Zustand.js - современный, невесомый, производительный и очень гибкий state manager
published: true
description: Zustand существует и развивается уже несколько лет, но многие разработчики до сих пор либо не знают о нем либо имеют не достаточно информации для того, что бы перейти к его использованию
tags: javascript, React, stateManagers, Zustand

cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gq8ou8ji2pwjfu2x3aal.png
# Use a ratio of 100:42 for best results.
# published_at: 2024-03-12 13:23 +0000
---
## Zustand.js - современный, невесомый, производительный и очень гибкий state manager.

Статья является расшифровкой части [доклада](https://budarin.github.io/zustand-presentation/?full#cover).

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

Zustand работает в любой окружении:

-   в коде React
-   в ванильном JavaScript

Его архитектура базируется на publish/subscribe объекте и реализации единственного хука для React.

## Легковесный

![Zustand bundlephobia](https://budarin.github.io/zustand-presentation/assets//bundlephobia.png)

Тут даже нечего добавить - 693 байта! Не знаю можно ли найти реализацию еще меньше чем эта.

## Простой

Zustand имеет простой синтаксис создания хранилища:

```typescript
const storeHook = create((set, get) => { ... store config ... });
```

Для примера создадим простое хранилище:

```typescript
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 вы можете создавать столько хранилищ, сколько вашей душе будет угодно.

```typescript
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 })),
}));
```

Созданный хук имеет простой синтаксис

```typescript
storeHook: (selector) => selectedState;
```

где selector - это простая функция для извлечению данных из переданного ей состояния хранилища

```typescript
const stateData = storeHook((state) => {
    // извлечение данных  из  state
    ...
    return selectedState;
});
```

Созданный React хук, легко использовать в компонентах

```typescript
function Counter() {
    const { count, increment } = useCounterStore((state) => state);

    return <button onClick={increment}>{count}</button>;
}
```

## Удобный

Хук для удобства имеет статические методы (методы объекта pub/sub) :

```typescript
{
    getState: () => state,
    setState: (newState) => void,
    subscribe: (callback) => void,
    ...
}
```

Статические методы очень удобно использовать в обработчиках

```typescript
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>;
}
```

В хранилище можно использовать асинхронные вызовы:

```typescript
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 };
        });
    },
}));
```

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

```typescript
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>;
}
```

Методы можно создавать и вне хранилища

```typescript
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 для отладки

## Производительный

А чтоже под капотом ?<br />
Привожу наивную реализацию хука Zustand, который отображает его логику

```typescript
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 для синхронной перерисовки дерева компонент

```typescript
function useStore(store, selector, equalityFn) {
    return useSyncExternalStoreWithSelector(
        store.subscribe,
        store.getState,
        store.getServerState || store.getInitialState,
        selector,
        equalityFn,
    );
}
```

В Concurrency mode в случае изменения данных в хранилище React будет прерывать параллельное построение дерева DOM и будет запускать синхронный рендер для того, чтобы отобразить новые изменения данных в хранилище - это гарантирует что ни одно изменение в хранилище не будет пропущено и будет отрисовано.
Команда React работает над тем, чтобы в будущем избежать грубого прерывания параллельной работы React и выполнять своевременную отрисовку изменений в контексте конкурентного режима.

## Методы оптимизации

Методы оптимизации вытекают из выше приведенного кода реализации хука - хук напрямую зависит от селектора, поэтому мы должны обеспечить постоянство ссылки на сам селектор и на данне возвращаемые им!

Их всего 3 простых метода оптимизации

1. если селектор не зависит от внутренних переменных - вынесите его за пределы компонента, таким образом селектор не будет создаваться каждый раз и ссылка на него будет постоянной

```typescript
const countSelector = (state) => state.count;

function Counter() {
    const count = useCounterStore(countSelector);
    return <div>{count}</div>;
}
```

2. если селектор имеет параметры - заключите его в useCallback - таким образом мы обеспечим постоянство ссылки на селектор в зависимости от значения параметра

```typescript
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>
    );
}
```

3. если селектор возвращает каждый раз новый объект - оберните вызов селектора в useShallow - если в новом объекте сами данные реально не изменились - хук useShallow выдаст ссылку на ранее сохраненный объект

```typescript
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

```typescript
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 напрямую

```typescript
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`.

## Лучшие практики

1. **Все связанные сущности держите в одном хранилище**

Zustand позволяет создавать не ограниченное количество хранилищ, но на практике отдельное хранилище нужно создавать только для данных, которые реально не связаны с данными в других хранилищах иначе вы вернетесь в прошлое - известная проблема model hell из MVC и FLUX (большое количество моделей имеющих хаотические связи с не прозрачной логикой взаимодействия с разнонаправленными потоками данных, как правило, всегда ведущих к зацикливанию обновлений и очень долгими отладками по вечерам для обнаружения этих зацикливаний - более подробно в докладе).

В настоящее время молодое поколение разработчиков, не работавших с MVC и FLUX и не изучавших теорию, создающих различные атомные, молекулярные, протонные, нейтронные и всякие кварковые стейт-менеджеры, наступают на грабли, которые индустрия прошла много лет назад. Связанные данные должны хранится в одном хранилище.
Иначе встает вопрос: зачем вы вынесли локальное состояние из компонента Counter в отдельное хранилище? Какие преимущества кроме неудобства и дополнительного кода, с его привнесенной сложностью, это дало?

Создание отдельных хранилищь оправдано лишь для:

-   реально не зависимых структур данных
-   для данных в микрофронтах

2. **Методы хранилища создавайте вне его описания**

Не смотря на то что Zustand позволяет создавать методы хранилища прямо в нем самом, рекомендую не создавать методы хранилища внутри него - если у вас в хранилище много сущностей или сущности сложные, а их методы обработки достаточно объемные - описание вашего хранилища станет не читаемым, не понимаемым и плохо поддерживаемым.

Размещение методов в отдельных модулях дает еще один бонус - кроме того что мы визуально изолируем код метода, что делает его легче для чтения и понимания, это позволяет легко тестировать метод в изоляции от самого хранилища.

3. **Доступ к данным в хранилище только при помощи его методов!**

Я много лет проектировал и сопровождал базы данных, и для меня не понятно почему в подавляющем большинстве проектов часть базы данных, представленная на клиенте хранилищем, являющегося частью бизнес логики данных, не обеспечивает эту бизнес логику!
Нет ни кода по обеспечению целостности данных, ни их непротиворечивости и безопасности!
Подавляющее количество хранилищ состояния приложения, которые я видел промышленной эксплуатации - это простые Json хранилки! Это не хранилище состояния, а именно простые хранилки json.

Если понять, что эти данные и есть то ядро приложения, вокруг которого строится приложение, то становится понятно, что как это хранилище реализовано - таким и будет надежность приложения. Если хранилище (ядро приложения) - это простая Json хранилка - то 99.99% что приложение будет лихорадить от большого количества багов. Так или иначе код по валидации данных будет размазан и дублирован по разным частям приложения. А отсутствие обеспечения целостности данных и их непротиворечивости гарантированно будет приводить к багам которые будут возникать постоянно в большой команде.

Чтобы создать надежное хранилище и тем самым ликвидировать большинство багов в приложении:

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

```typescript
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 лет мы наконец-то вышли на правильную дорогу: однонаправленный поток данных (компонент -> реакция юзера -> изменение данных в хранилище -> хук для отображения изменений) и хранение связанных структур данных в одном хранилище.<br />
По пути нам пришлось вначале изобрести FLUX, и затем уйти еще дальше в сторону от простого решения на Redux, параллельно на ощупь искать правильное решение при помощи тысяч реализаций стейт менеджеров.

Но хорошо то, что хорошо заканчивается!

Zustand - это просто мечта разработчика!

-   компактный
-   простой в использовании
-   не добавляющий когнитивной нагрузки
-   производительный
-   легко оптимизируемый
-   легко масштабируемый
-   имеющий легко поддерживаемый код
-   исключающий проблемы типа Zombie children и context loss
-   поддерживающий работу в React concurrency mode

Мы используем Zustand практически с момента его появления на свет и испытываем только положительные эмоции от его применения.

Начните активно использовать Zustand в своих проектах - вы будете приятно удивлены простотой и удобством работы с ним.