Инициализация хранилища на основе IndexedDB до момента рендера компонентов позволяет избежать целого ряда проблем:
Библиотека idb-keyval предоставляет простой API поверх
IndexedDB, но сама по себе не решает вопроса тайминга инициализации. Эта
задача ложится на архитектуру приложения.
Ключевая идея — получить все необходимые данные до первого рендера и передать их в приложение как уже готовое состояние.
Основные шаги:
По умолчанию idb-keyval использует базу данных
keyval-store. Для контроля над именами и версиями создаётся
собственный store:
import { Store } from 'idb-keyval';
export const appStore = new Store('app-db', 'app-store');
Параметры:
app-db — имя базы данных;app-store — имя object store внутри IndexedDB.Такой подход необходим для:
Извлечение данных из IndexedDB всегда асинхронно. Использование
await позволяет синхронизировать процесс:
import { get, set } from 'idb-keyval';
import { appStore } from './store';
export async function initializeStorage() {
let settings = await get('settings', appStore);
if (!settings) {
settings = {
theme: 'light',
language: 'ru'
};
await set('settings', settings, appStore);
}
return { settings };
}
Особенности:
Инициализация должна выполняться до вызова рендера UI. В чистом
JavaScript это реализуется через async-обёртку:
import { initializeStorage } from './init';
async function bootstrap() {
const initialData = await initializeStorage();
startApp(initialData);
}
bootstrap();
Функция startApp запускает приложение, уже имея все
необходимые данные.
В React важно исключить рендер до получения данных. Используется внешний bootstrap:
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { initializeStorage } from './init';
async function bootstrap() {
const initialData = await initializeStorage();
const root = createRoot(document.getElementById('root'));
root.render(<App initialData={initialData} />);
}
bootstrap();
Преимущества:
useEffect для первичной
загрузки.import { createStore } from 'redux';
import rootReducer from './reducers';
import { initializeStorage } from './init';
async function bootstrap() {
const initialData = await initializeStorage();
const store = createStore(rootReducer, initialData);
renderApp(store);
}
import create from 'zustand';
import { initializeStorage } from './init';
export const useStore = create(() => ({}));
async function bootstrap() {
const initialData = await initializeStorage();
useStore.setState(initialData);
renderApp();
}
В обоих случаях:
Если требуется загрузка нескольких ключей, используется
Promise.all:
import { get } from 'idb-keyval';
import { appStore } from './store';
export async function initializeStorage() {
const [settings, user, cache] = await Promise.all([
get('settings', appStore),
get('user', appStore),
get('cache', appStore)
]);
return {
settings: settings || {},
user: user || null,
cache: cache || {}
};
}
Это уменьшает время инициализации.
IndexedDB может быть недоступен (например, в режиме инкогнито). Обработка ошибок обязательна:
export async function initializeStorage() {
try {
const settings = await get('settings', appStore);
return { settings: settings || {} };
} catch (error) {
console.error('Storage initialization failed:', error);
return {
settings: {
theme: 'light'
}
};
}
}
Поведение:
Чтобы избежать повторных вызовов, можно кэшировать результат:
let cachedInit = null;
export async function initializeStorage() {
if (cachedInit) return cachedInit;
cachedInit = (async () => {
const settings = await get('settings', appStore);
return { settings: settings || {} };
})();
return cachedInit;
}
Это особенно полезно при повторных вызовах в сложных архитектурах.
Ранняя инициализация (до рендера):
Lazy-инициализация (после рендера):
Выбор зависит от требований, но для критичных данных предпочтительна ранняя инициализация.
Рекомендуемая структура:
/storage
store.js
init.js
keys.js
Пример keys.js:
export const STORAGE_KEYS = {
SETTINGS: 'settings',
USER: 'user',
CACHE: 'cache'
};
Использование констант предотвращает ошибки и упрощает рефакторинг.
При изменении структуры данных требуется миграция:
export async function initializeStorage() {
let settings = await get('settings', appStore);
if (settings && !settings.version) {
settings = {
...settings,
version: 2,
newField: true
};
await set('settings', settings, appStore);
}
return { settings };
}
Подход:
Факторы, влияющие на скорость:
Рекомендации:
Promise.all);Store).Такая схема обеспечивает детерминированное поведение интерфейса и упрощает дальнейшую работу с локальным хранилищем.