Использование совместно с Redux и Vuex

Работа с клиентским хранилищем в современных веб-приложениях редко ограничивается прямыми вызовами API браузера. При использовании архитектур управления состоянием, таких как Redux и Vuex, возникает необходимость синхронизации состояния между памятью приложения и долговременным хранилищем. localForage выступает в роли абстракции над IndexedDB, WebSQL и localStorage, позволяя организовать асинхронное персистентное хранилище без привязки к конкретной реализации.


Архитектурная роль localForage в потоках данных

В классическом SPA данные проходят следующий путь:

  1. UI инициирует действие
  2. Action/Mutation обновляет store
  3. Store обновляет состояние интерфейса
  4. Побочно выполняется синхронизация с сервером или локальным хранилищем

localForage в этой цепочке почти всегда используется как слой side effects persistence layer. Он не должен заменять Redux/Vuex, а только дополнять их, обеспечивая:

  • сохранение состояния между перезагрузками
  • кэширование API-ответов
  • оффлайн-доступ
  • восстановление состояния приложения

Базовый принцип интеграции

Общая идея одинакова для обеих систем:

  • Store является источником истины (single source of truth)
  • localForage — долговременное зеркало состояния
  • синхронизация происходит через middleware (Redux) или plugins (Vuex)

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

Middleware-подход

В Redux стандартным способом подключения побочных эффектов является middleware.

localForage подключается через middleware, которое отслеживает actions и сохраняет состояние.

Пример middleware синхронизации

import localforage from "localforage";

const persistenceMiddleware = store => next => action => {
  const result = next(action);

  const state = store.getState();

  localforage.setItem("app_state", state);

  return result;
};

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

  • сохраняется весь state
  • простая реализация
  • риск избыточных записей
  • отсутствие контроля над частотой записи

Оптимизированная стратегия: выборочная персистенция

В реальных приложениях сохраняется только часть состояния:

const persistenceMiddleware = store => next => action => {
  const result = next(action);

  const { auth, settings, cache } = store.getState();

  localforage.setItem("persisted_state", {
    auth,
    settings,
    cache
  });

  return result;
};

Преимущества

  • меньше нагрузка на IndexedDB
  • быстрее восстановление
  • меньше конфликтов версий структуры состояния

Восстановление состояния Redux

Инициализация store с асинхронной гидратацией:

import { createStore } from "redux";
import localforage from "localforage";
import rootReducer from "./reducers";

export async function configureStore() {
  const persisted = await localforage.getItem("persisted_state");

  const store = createStore(
    rootReducer,
    persisted || undefined
  );

  return store;
}

Важный аспект

Redux store создаётся синхронно, поэтому в реальных приложениях часто используют:

  • splash screen
  • bootstrap loader
  • или временный preloaded state

Debounce стратегия записи

Частая ошибка — запись в localForage на каждое действие.

Решение: debounce слой

import debounce from "lodash.debounce";
import localforage from "localforage";

const saveState = debounce((state) => {
  localforage.setItem("persisted_state", state);
}, 300);

export const persistenceMiddleware = store => next => action => {
  const result = next(action);
  saveState(store.getState());
  return result;
};

Сегментированное хранение Redux state

Более продвинутая архитектура:

  • auth → отдельный key
  • ui → отдельный key
  • cache → отдельный key
localforage.setItem("auth", state.auth);
localforage.setItem("ui", state.ui);

Плюсы

  • точечные обновления
  • меньше конфликтов при миграции схемы
  • проще инвалидировать кэш

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

Во Vuex основной механизм расширения — plugins.


Базовый Vuex plugin

import localforage from "localforage";

export function createPersistPlugin() {
  return store => {
    store.subscribe((mutation, state) => {
      localforage.setItem("vuex_state", state);
    });
  };
}

Подключение

import Vuex from "vuex";
import { createPersistPlugin } from "./persist";

export default new Vuex.Store({
  state: {},
  mutations: {},
  plugins: [createPersistPlugin()]
});

Гидратация Vuex store

Vuex допускает замену state после инициализации:

localforage.getItem("vuex_state").then(savedState => {
  if (savedState) {
    store.replaceState(savedState);
  }
});

Фильтрация мутаций

Не все mutations должны приводить к записи.

const whitelist = ["SET_USER", "SET_SETTINGS"];

store.subscribe((mutation, state) => {
  if (whitelist.includes(mutation.type)) {
    localforage.setItem("vuex_state", state);
  }
});

Оптимизация записи через batching

При высокой частоте мутаций:

import debounce from "lodash.debounce";

const save = debounce((state) => {
  localforage.setItem("vuex_state", state);
}, 200);

store.subscribe((mutation, state) => {
  save(state);
});

Разделение Vuex modules

Рекомендуемая практика:

store.subscribe((mutation, state) => {
  localforage.setItem("auth", state.auth);
  localforage.setItem("preferences", state.preferences);
});

Восстановление:

Promise.all([
  localforage.getItem("auth"),
  localforage.getItem("preferences")
]).then(([auth, preferences]) => {
  store.replaceState({
    ...store.state,
    auth,
    preferences
  });
});

Общие архитектурные паттерны

1. Store-driven persistence

Store инициирует сохранение:

  • middleware (Redux)
  • plugin (Vuex)

2. Storage-driven hydration

localForage используется только на старте приложения:

  • восстановление состояния
  • миграция схем

3. Hybrid caching model

  • Redux/Vuex = runtime state
  • localForage = persistent cache
  • server = source of truth

Версионирование состояния

localForage не управляет схемами, поэтому вводится versioning:

const PERSIST_VERSION = 2;

const data = {
  version: PERSIST_VERSION,
  state
};

localforage.setItem("app_state", data);

При загрузке:

const saved = await localforage.getItem("app_state");

if (saved?.version !== PERSIST_VERSION) {
  await localforage.clear();
}

Ошибки и ограничения интеграции

1. Асинхронность

Redux и Vuex изначально синхронные системы, localForage — нет.

2. Конкуренция записей

При частых mutation/action возможны гонки записей.

3. Большие объекты состояния

Полный state может превышать разумные лимиты IndexedDB.

4. SSR-конфликты

На сервере localForage недоступен, требуется проверка окружения:

if (typeof window !== "undefined") {
  localforage.setItem(...)
}

Практическая стратегия выбора подхода

  • небольшие приложения → full state persistence
  • средние приложения → selective persistence
  • крупные системы → module-based persistence + versioning + debounce + migration layer

Итоговая модель взаимодействия

  • Redux/Vuex управляет состоянием в памяти
  • localForage хранит долговременную копию
  • middleware/plugin обеспечивает синхронизацию
  • bootstrap слой восстанавливает состояние
  • versioning контролирует эволюцию схемы данных