Работа с клиентским хранилищем в современных веб-приложениях редко ограничивается прямыми вызовами API браузера. При использовании архитектур управления состоянием, таких как Redux и Vuex, возникает необходимость синхронизации состояния между памятью приложения и долговременным хранилищем. localForage выступает в роли абстракции над IndexedDB, WebSQL и localStorage, позволяя организовать асинхронное персистентное хранилище без привязки к конкретной реализации.
В классическом SPA данные проходят следующий путь:
localForage в этой цепочке почти всегда используется как слой side effects persistence layer. Он не должен заменять Redux/Vuex, а только дополнять их, обеспечивая:
Общая идея одинакова для обеих систем:
В Redux стандартным способом подключения побочных эффектов является middleware.
localForage подключается через middleware, которое отслеживает actions и сохраняет состояние.
import localforage from "localforage";
const persistenceMiddleware = store => next => action => {
const result = next(action);
const state = store.getState();
localforage.setItem("app_state", state);
return result;
};
В реальных приложениях сохраняется только часть состояния:
const persistenceMiddleware = store => next => action => {
const result = next(action);
const { auth, settings, cache } = store.getState();
localforage.setItem("persisted_state", {
auth,
settings,
cache
});
return result;
};
Инициализация 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 создаётся синхронно, поэтому в реальных приложениях часто используют:
Частая ошибка — запись в 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;
};
Более продвинутая архитектура:
localforage.setItem("auth", state.auth);
localforage.setItem("ui", state.ui);
Во Vuex основной механизм расширения — plugins.
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 допускает замену 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);
}
});
При высокой частоте мутаций:
import debounce from "lodash.debounce";
const save = debounce((state) => {
localforage.setItem("vuex_state", state);
}, 200);
store.subscribe((mutation, state) => {
save(state);
});
Рекомендуемая практика:
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
});
});
Store инициирует сохранение:
localForage используется только на старте приложения:
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();
}
Redux и Vuex изначально синхронные системы, localForage — нет.
При частых mutation/action возможны гонки записей.
Полный state может превышать разумные лимиты IndexedDB.
На сервере localForage недоступен, требуется проверка окружения:
if (typeof window !== "undefined") {
localforage.setItem(...)
}