Работа с асинхронным клиентским хранилищем в React требует учета двух ключевых факторов: жизненного цикла компонентов и асинхронной природы операций чтения/записи. Библиотека localForage предоставляет единый API поверх IndexedDB, WebSQL и localStorage, но сама по себе не решает архитектурные задачи React-приложений. Поэтому основным строительным блоком становится инкапсуляция логики в кастомные хуки.
Типовой поток данных включает три стадии: инициализация состояния, асинхронное чтение из хранилища и синхронизация при изменениях.
Наиболее универсальный паттерн — создание хука, который связывает ключ в хранилище и состояние компонента.
import { useEffect, useState, useCallback } from "react";
import localforage from "localforage";
export function useLocalForage(key, initialValue = null) {
const [value, setValue] = useState(initialValue);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true;
localforage.getItem(key).then((storedValue) => {
if (!isMounted) return;
if (storedValue === null || storedValue === undefined) {
setValue(initialValue);
} else {
setValue(storedValue);
}
setLoading(false);
});
return () => {
isMounted = false;
};
}, [key, initialValue]);
const setStoredValue = useCallback(
async (newValue) => {
setValue(newValue);
await localforage.setItem(key, newValue);
},
[key]
);
const remove = useCallback(async () => {
setValue(initialValue);
await localforage.removeItem(key);
}, [key, initialValue]);
return { value, setValue: setStoredValue, remove, loading };
}
Ключевая идея заключается в разделении UI-состояния и состояния хранилища при сохранении синхронного интерфейса для компонентов.
Асинхронная загрузка из IndexedDB приводит к промежуточному состоянию неопределенности. Без явного флага загрузки компонент может некорректно отрисовать UI.
Распространенный паттерн:
loading = true до завершения getItemif (loading) {
return <div>Загрузка данных...</div>;
}
Более строгая модель исключает отображение initialValue
до фактической проверки хранилища.
localForage не является реактивным хранилищем. Изменение значения в одном месте не уведомляет другие части приложения. Для синхронизации применяется один из следующих подходов:
Использование кастомного EventEmitter или
window-событий.
const STORAGE_EVENT = "localforage-change";
export function broadcastChange(key, value) {
window.dispatchEvent(
new CustomEvent(STORAGE_EVENT, {
detail: { key, value }
})
);
}
В хуке добавляется подписка:
useEffect(() => {
const handler = (event) => {
if (event.detail.key === key) {
setValue(event.detail.value);
}
};
window.addEventListener(STORAGE_EVENT, handler);
return () => window.removeEventListener(STORAGE_EVENT, handler);
}, [key]);
Более предсказуемый подход — централизованный React Context, который инкапсулирует доступ к localForage.
const StorageContext = React.createContext(null);
Провайдер управляет кешем значений и синхронизацией.
Преимущества:
Недостаток — рост сложности при масштабировании ключей.
localForage работает асинхронно и имеет задержку даже при использовании IndexedDB. Для оптимизации применяется in-memory cache.
const cache = new Map();
export async function getCachedItem(key) {
if (cache.has(key)) return cache.get(key);
const value = await localforage.getItem(key);
cache.set(key, value);
return value;
}
В React-хуке это снижает количество обращений к IndexedDB и уменьшает лаги при повторных рендерах.
Паттерн оптимистического обновления особенно полезен для UI, где задержка записи не должна блокировать взаимодействие.
const setValue = useCallback(async (newValue) => {
setValueState(newValue);
try {
await localforage.setItem(key, newValue);
} catch (e) {
// откат при ошибке
setValueState(prevValue);
}
}, [key]);
Особенность: требуется хранение предыдущего значения для возможного rollback.
Расширенная версия интеграции объединяет загрузку, запись, кэш и обработку ошибок.
import { useEffect, useState, useCallback, useRef } from "react";
import localforage from "localforage";
export function usePersistentState(key, initialValue) {
const [state, setState] = useState(initialValue);
const [ready, setReady] = useState(false);
const prevRef = useRef(initialValue);
useEffect(() => {
let alive = true;
(async () => {
const stored = await localforage.getItem(key);
if (!alive) return;
if (stored !== null && stored !== undefined) {
setState(stored);
prevRef.current = stored;
}
setReady(true);
})();
return () => {
alive = false;
};
}, [key]);
const upd ate = useCallback(async (value) => {
prevRef.current = state;
setState(value);
try {
await localforage.setItem(key, value);
} catch (e) {
setState(prevRef.current);
}
}, [key, state]);
const clear = useCallback(async () => {
prevRef.current = state;
setState(initialValue);
await localforage.removeItem(key);
}, [key, state, initialValue]);
return {
state,
setState: update,
clear,
ready
};
}
localForage поддерживает хранение объектов без ручной сериализации, но при интеграции с React часто требуется контроль структуры данных.
Типовые проблемы:
Решение — явная нормализация:
const serialize = (data) => ({
...data,
createdAt: data.createdAt?.toISOString?.() ?? null
});
const deserialize = (data) => ({
...data,
createdAt: data.createdAt ? new Date(data.createdAt) : null
});
При изменении структуры данных необходимо учитывать миграции.
const STORAGE_VERSION = 2;
async function migrate(key, data) {
if (!data) return data;
if (!data.__version || data.__version < STORAGE_VERSION) {
data = {
...data,
__version: STORAGE_VERSION
};
}
return data;
}
Интеграция миграции происходит в слое получения данных:
const raw = await localforage.getItem(key);
const migrated = await migrate(key, raw);
В средах с серверным рендерингом localForage доступен только на клиенте. Это требует разделения логики:
const isClient = typeof window !== "undefined";
Хук должен избегать вызова localForage при SSR:
useEffect(() => {
if (!isClient) return;
...
}, [key]);
Для сложных приложений используется стратегия lazy hydration:
Это снижает time-to-interactive, особенно при больших объемах данных в IndexedDB.
localForage можно использовать как persistence layer поверх query-кэша.
Идея:
async function persistQuery(key, data) {
await localforage.setItem(key, {
data,
timestamp: Date.now()
});
}
При инициализации:
const cached = await localforage.getItem(key);
if (cached && Date.now() - cached.timestamp < TTL) {
return cached.data;
}
Асинхронные операции могут приводить к гонкам при быстром переключении ключей.
Решение — контроль актуальности запроса:
useEffect(() => {
let active = true;
localforage.getItem(key).then((value) => {
if (!active) return;
setState(value);
});
return () => {
active = false;
};
}, [key]);
Архитектурно интеграция localForage с React делится на три уровня:
Такое разделение снижает связанность и упрощает тестирование.