Гидратация состояния — это процесс восстановления данных приложения на клиенте после первоначальной загрузки. В контексте браузерных приложений это означает извлечение ранее сохранённого состояния (например, из IndexedDB) и его внедрение в текущую сессию, чтобы обеспечить непрерывность пользовательского опыта.
При использовании библиотеки idb-keyval гидратация реализуется за счёт простого API для работы с IndexedDB, позволяющего сохранять и извлекать данные асинхронно, без необходимости напрямую взаимодействовать с низкоуровневым API браузера.
Библиотека предоставляет минималистичный интерфейс:
get(key) — получение значенияset(key, value) — сохранение значенияdel(key) — удалениеclear() — очистка хранилищаЭти методы позволяют организовать механизм восстановления состояния без сложной инфраструктуры.
Ключевая идея — при загрузке приложения извлечь данные из IndexedDB и синхронизировать их с состоянием UI или state-менеджера.
import { set } from 'idb-keyval';
async function saveState(state) {
await set('app-state', state);
}
Состояние может включать:
import { get } from 'idb-keyval';
async function loadState() {
const state = await get('app-state');
return state || {};
}
Гидратация должна происходить до рендера UI, чтобы избежать “мигания” интерфейса и несогласованности данных.
Пример:
async function bootstrapApp() {
const persistedState = await loadState();
const app = createApp({
initialState: persistedState
});
app.mount('#app');
}
bootstrapApp();
Не всегда требуется восстанавливать всё состояние. Более эффективный подход — выборочная гидратация.
const keys = ['user', 'settings', 'cart'];
async function hydratePartial() {
const result = {};
for (const key of keys) {
result[key] = await get(key);
}
return result;
}
Преимущества:
Часть данных может быть загружена позже, по мере необходимости.
async function getUserData() {
let user = await get('user');
if (!user) {
user = await fetch('/api/user').then(r => r.json());
await set('user', user);
}
return user;
}
Такой подход:
Гидратация — это не одноразовое действие. Важно поддерживать синхронизацию между:
Пример:
function subscribeToStore(store) {
store.subscribe(async (state) => {
await set('app-state', state);
});
}
Со временем структура данных может изменяться. Для этого вводится версия состояния.
const CURRENT_VERSION = 2;
async function loadState() {
const data = await get('app-state');
if (!data) return {};
if (data.version !== CURRENT_VERSION) {
return migrate(data);
}
return data;
}
Пример миграции:
function migrate(oldState) {
if (oldState.version === 1) {
return {
...oldState,
newField: null,
version: 2
};
}
return oldState;
}
IndexedDB — асинхронное API, и ошибки возможны:
async function safeLoad() {
try {
return await get('app-state');
} catch (e) {
console.error('Ошибка загрузки состояния', e);
return {};
}
}
Гидратация часто используется для повторного использования данных API:
async function fetchWithCache(key, url) {
const cached = await get(key);
if (cached) return cached;
const response = await fetch(url);
const data = await response.json();
await set(key, data);
return data;
}
Без контроля IndexedDB может разрастаться.
async function clearOldCache() {
const timestamp = await get('cache-time');
if (Date.now() - timestamp > 86400000) {
await clear();
}
}
idb-keyval позволяет создавать отдельные store:
import { createStore, set, get } from 'idb-keyval';
const customStore = createStore('my-db', 'my-store');
await set('key', 'value', customStore);
const value = await get('key', customStore);
Это полезно для:
store.subscribe(async () => {
const state = store.getState();
await set('redux-state', state);
});
Гидратация:
const preloadedState = await get('redux-state');
const store = createStore(reducer, preloadedState);
const useStore = create((set) => ({
data: null,
hydrate: async () => {
const data = await get('data');
set({ data });
}
}));
Рекомендации:
function debounce(fn, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
const save = debounce((state) => {
set('app-state', state);
}, 300);
IndexedDB доступна из JavaScript, поэтому:
Гидратация может повлиять на рендеринг:
let isReady = false;
async function init() {
const state = await loadState();
render(state);
isReady = true;
}
В UI:
if (!isReady) {
return <Loading />;
}
Иногда состояние восстанавливается только при обращении:
class Store {
constructor() {
this.cache = null;
}
async getState() {
if (!this.cache) {
this.cache = await get('state');
}
return this.cache;
}
}
Для тестов удобно мокать idb-keyval:
jest.mock('idb-keyval', () => ({
get: jest.fn(),
set: jest.fn()
}));
type AppState = {
user: string;
theme: string;
};
async function loadState(): Promise<AppState> {
const state = await get('app-state');
return state as AppState;
}
Centralized hydration
Modular hydration
Hybrid
Однако для гидратации состояния это компенсируется:
async function initApp() {
const state = await loadState();
const store = createStore(state);
store.subscribe(
debounce((newState) => {
set('app-state', newState);
}, 200)
);
render(store);
}
Такой подход обеспечивает: