Персистентность состояния Zustand через idb-keyval

idb-keyval — это легковесная библиотека для работы с IndexedDB в браузере, упрощающая хранение ключ-значение. Она идеально подходит для использования совместно с Zustand для сохранения состояния приложения между сессиями. Библиотека предоставляет простые функции: set, get, del, clear, keys, что позволяет легко управлять данными без необходимости писать сложные запросы IndexedDB вручную.

Пример базового использования:

import { set, get, del, clear } from 'idb-keyval';

// Сохранение значения
await set('theme', 'dark');

// Получение значения
const theme = await get('theme'); // 'dark'

// Удаление значения
await del('theme');

// Очистка всех данных
await clear();

Интеграция с Zustand

Zustand позволяет создать глобальное состояние приложения. Чтобы сохранять его в IndexedDB, можно использовать middleware или кастомные функции для синхронизации состояния.

Пример базовой интеграции:

import create from 'zustand';
import { persist } from 'zustand/middleware';
import { set, get } from 'idb-keyval';

const useStore = create(
  persist(
    (set, get) => ({
      theme: 'light',
      toggleTheme: () => set({ theme: get().theme === 'light' ? 'dark' : 'light' }),
    }),
    {
      name: 'app-state',
      getStorage: () => ({
        getItem: async (name) => {
          const value = await get(name);
          return value ? JSON.stringify(value) : null;
        },
        setItem: async (name, value) => {
          await set(name, JSON.parse(value));
        },
        removeItem: async (name) => {
          await del(name);
        },
      }),
    }
  )
);

Ключевые моменты:

  • Используется JSON-сериализация для совместимости с Zustand, так как persist ожидает работу с localStorage-подобными объектами.
  • getStorage позволяет указать кастомное хранилище, в данном случае idb-keyval.
  • Все операции getItem, setItem и removeItem асинхронные, что соответствует природе IndexedDB.

Асинхронная синхронизация состояния

Для более сложных состояний, содержащих массивы или объекты, важно правильно обрабатывать асинхронные операции. Например, если необходимо загружать состояние при инициализации приложения:

import { get } from 'idb-keyval';

async function initializeState() {
  const savedState = await get('app-state');
  return savedState || { theme: 'light', user: null };
}

const useStore = create((set) => ({
  theme: 'light',
  user: null,
  initialize: async () => {
    const state = await initializeState();
    set(state);
  },
  toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
}));

Особенности подхода:

  • Асинхронная инициализация позволяет гарантировать актуальность данных.
  • Можно комбинировать с подпиской на изменения состояния, чтобы автоматически сохранять их в IndexedDB:
useStore.subscribe((state) => {
  set('app-state', state);
});

Работа с сложными структурами данных

idb-keyval поддерживает любые сериализуемые объекты JavaScript, включая массивы, объекты с вложенностью и даже Map/Set при дополнительной сериализации. Рекомендуется всегда использовать JSON при интеграции с Zustand, чтобы избежать проблем с восстановлением состояния:

const complexState = {
  user: { id: 1, name: 'Alice' },
  preferences: { theme: 'dark', notifications: true },
  history: [1, 2, 3],
};

// Сохранение
await set('complex-state', JSON.stringify(complexState));

// Загрузка
const savedState = JSON.parse(await get('complex-state'));

Подписки и автоматическое сохранение

Для поддержания актуального состояния удобно использовать подписки Zustand. Каждое изменение состояния можно автоматически сохранять в IndexedDB, минимизируя риск потери данных:

const unsubscribe = useStore.subscribe(
  (state) => {
    set('app-state', state);
  }
);

// Отмена подписки при необходимости
unsubscribe();

Преимущества такого подхода:

  • Автоматическая персистентность без ручных вызовов set.
  • Возможность масштабирования: можно подписаться на отдельные свойства состояния для экономии операций записи.

Кастомизация ключей и пространств имен

Для разных частей приложения полезно использовать отдельные ключи в IndexedDB, чтобы избежать конфликтов:

await set('user-settings', { theme: 'dark', language: 'ru' });
await set('session-data', { token: 'abcd1234', expires: 1680000000 });

Можно создать утилиты для генерации ключей:

const makeKey = (namespace, key) => `${namespace}:${key}`;

await set(makeKey('user', 'theme'), 'dark');
await set(makeKey('user', 'language'), 'ru');

Это обеспечивает модульность и простоту управления различными частями состояния.

Практические рекомендации

  • Использовать await при всех операциях с idb-keyval, чтобы избежать состояния гонки.
  • Для больших состояний разделять данные на несколько ключей, а не хранить всё в одном объекте.
  • Обрабатывать возможные ошибки с try/catch, так как IndexedDB может быть недоступен в некоторых браузерах или режимах приватного просмотра.
  • Для производительности можно внедрять дебаунс при сохранении часто изменяющихся состояний.

Эти методы позволяют реализовать полноценное персистентное состояние Zustand через idb-keyval с минимальными усилиями, обеспечивая стабильность и масштабируемость приложения.