Интеграция с React: хуки и паттерны

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

Типовой поток данных включает три стадии: инициализация состояния, асинхронное чтение из хранилища и синхронизация при изменениях.


Инкапсуляция localForage в кастомный хук

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

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 до завершения getItem
  • отложенный рендер зависимых компонентов
  • fallback-значение до завершения гидратации
if (loading) {
  return <div>Загрузка данных...</div>;
}

Более строгая модель исключает отображение initialValue до фактической проверки хранилища.


Паттерн синхронизации нескольких компонентов

localForage не является реактивным хранилищем. Изменение значения в одном месте не уведомляет другие части приложения. Для синхронизации применяется один из следующих подходов:

1. Event-based синхронизация

Использование кастомного 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]);

2. Общий слой состояния (Context)

Более предсказуемый подход — централизованный React Context, который инкапсулирует доступ к localForage.

const StorageContext = React.createContext(null);

Провайдер управляет кешем значений и синхронизацией.

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

  • единая точка доступа
  • кэширование
  • предсказуемое обновление UI

Недостаток — рост сложности при масштабировании ключей.


Кэширование значений в памяти

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.


Хук usePersistentState

Расширенная версия интеграции объединяет загрузку, запись, кэш и обработку ошибок.

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 часто требуется контроль структуры данных.

Типовые проблемы:

  • хранение нестабильных объектов (Date, Map, Se t)
  • несовместимость версий схемы
  • разрастание структуры

Решение — явная нормализация:

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);

Интеграция с SSR и гидратацией

В средах с серверным рендерингом localForage доступен только на клиенте. Это требует разделения логики:

  • на сервере используется fallback
  • на клиенте происходит гидратация из IndexedDB
const isClient = typeof window !== "undefined";

Хук должен избегать вызова localForage при SSR:

useEffect(() => {
  if (!isClient) return;
  ...
}, [key]);

Паттерн ленивой инициализации

Для сложных приложений используется стратегия lazy hydration:

  • компонент монтируется мгновенно
  • данные подтягиваются после первого рендера
  • UI постепенно обновляется

Это снижает time-to-interactive, особенно при больших объемах данных в IndexedDB.


Интеграция с React Query-стилем

localForage можно использовать как persistence layer поверх query-кэша.

Идея:

  • React Query управляет состоянием запроса
  • localForage хранит persisted cache
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;
}

Предотвращение race conditions

Асинхронные операции могут приводить к гонкам при быстром переключении ключей.

Решение — контроль актуальности запроса:

useEffect(() => {
  let active = true;

  localforage.getItem(key).then((value) => {
    if (!active) return;
    setState(value);
  });

  return () => {
    active = false;
  };
}, [key]);

Разделение слоев ответственности

Архитектурно интеграция localForage с React делится на три уровня:

  • UI слой: компоненты и хуки
  • сервисный слой: работа с ключами и миграциями
  • storage слой: прямой localForage API

Такое разделение снижает связанность и упрощает тестирование.