Использование с Vue: реактивные обёртки

Базовая модель реактивности при работе с IndexedDB

Работа с IndexedDB через Dexie.js всегда опирается на асинхронные операции, возвращающие Promise. В контексте Vue 3 это создает разрыв между императивным доступом к данным и реактивной моделью представления. Основная задача интеграции — превратить результаты запросов Dexie в реактивные источники состояния, автоматически обновляющиеся при изменениях базы.

Ключевое свойство Dexie — возможность построения подписок на изменения через механизмы наблюдения (включая liveQuery). Vue, в свою очередь, оперирует ref, reactive, computed и системой эффектов, что позволяет связать результат запроса с UI без ручного обновления состояния.


Преобразование Dexie-запросов в реактивное состояние

Обёртка через ref и асинхронную загрузку

Самый прямолинейный способ интеграции — загрузка данных при инициализации компонента и помещение результата в ref.

import { ref, onMounted } fr om 'vue'
import { db } from './db'

export function useTodos() {
  const todos = ref([])
  const loading = ref(true)

  const load = async () => {
    loading.value = true
    todos.value = await db.todos.toArray()
    loading.value = false
  }

  onMounted(load)

  return { todos, loading, reload: load }
}

Такой подход прост, но не обеспечивает автоматического обновления при изменении базы данных.


Реактивное обновление через liveQuery

Принцип наблюдаемого запроса

Dexie предоставляет liveQuery, который преобразует запрос в поток изменений. При каждом изменении данных запрос пересчитывается, а подписчики получают новое значение.

import { liveQuery } from 'dexie'
import { ref, onMounted, onUnmounted } from 'vue'
import { db } from './db'

export function useLiveTodos() {
  const todos = ref([])
  let subscription = null

  onMounted(() => {
    subscription = liveQuery(() => db.todos.toArray())
      .subscribe({
        next(result) {
          todos.value = result
        },
        error(err) {
          console.error(err)
        }
      })
  })

  onUnmounted(() => {
    subscription?.unsubscribe()
  })

  return { todos }
}

Механизм liveQuery устраняет необходимость ручного обновления состояния, обеспечивая реактивность на уровне источника данных.


Инкапсуляция подписок в composable

Унифицированная реактивная обёртка

Повторяющаяся логика подписки может быть вынесена в универсальный composable, принимающий функцию запроса.

import { ref, onMounted, onUnmounted } from 'vue'
import { liveQuery } from 'dexie'

export function useDexieQuery(queryFn) {
  const state = ref(null)
  const error = ref(null)
  let subscription = null

  onMounted(() => {
    subscription = liveQuery(queryFn).subscribe({
      next(value) {
        state.value = value
        error.value = null
      },
      error(err) {
        error.value = err
      }
    })
  })

  onUnmounted(() => {
    subscription?.unsubscribe()
  })

  return { state, error }
}

Использование:

import { db } from './db'
import { useDexieQuery } from './useDexieQuery'

export function useActiveUsers() {
  return useDexieQuery(() =>
    db.users.wh ere('active').equals(1).toArray()
  )
}

Интеграция с computed-логикой Vue

Производные данные поверх Dexie-запросов

Результаты liveQuery могут выступать базой для вычисляемых значений через computed.

import { computed } fr om 'vue'
import { useDexieQuery } from './useDexieQuery'
import { db } from './db'

export function useStats() {
  const { state: todos } = useDexieQuery(() =>
    db.todos.toArray()
  )

  const completedCount = computed(() =>
    todos.value ? todos.value.filter(t => t.done).length : 0
  )

  return { todos, completedCount }
}

Такой подход разделяет уровень хранения данных и уровень их представления.


Синхронизация нескольких запросов

Комбинирование реактивных источников

При работе с несколькими таблицами Dexie часто требуется агрегировать данные.

import { useDexieQuery } from './useDexieQuery'
import { db } from './db'
import { computed } from 'vue'

export function useDashboard() {
  const users = useDexieQuery(() => db.users.toArray())
  const posts = useDexieQuery(() => db.posts.toArray())

  const summary = computed(() => ({
    usersCount: users.state.value?.length ?? 0,
    postsCount: posts.state.value?.length ?? 0
  }))

  return { users, posts, summary }
}

Каждый источник остаётся независимым, но итоговое состояние формируется реактивно.


Работа с мутациями и автоматическое обновление

Обновление данных через Dexie и реактивная синхронизация

При использовании liveQuery любые операции записи автоматически отражаются в подписках.

import { db } from './db'

export async function addTodo(title) {
  await db.todos.add({
    title,
    done: false,
    createdAt: Date.now()
  })
}

export async function toggleTodo(id, done) {
  await db.todos.update(id, { done })
}

Подписки, созданные через liveQuery, реагируют без дополнительного кода обновления.


Управление жизненным циклом подписок

Предотвращение утечек памяти

Подписки Dexie требуют явного завершения при уничтожении компонента.

onUnmounted(() => {
  subscription?.unsubscribe()
})

При использовании универсального composable важно гарантировать:

  • единый канал подписки;
  • корректный unsubscribe;
  • отсутствие повторных подписок при повторном монтировании.

Оптимизация реактивных запросов

Снижение количества пересчётов

Частые обновления базы могут приводить к множественным пересчётам liveQuery. Для оптимизации применяются:

  • фильтрация данных на уровне запроса (where, filter);
  • индексы Dexie для ускорения выборок;
  • разделение крупных запросов на более узкие;
  • минимизация вычислений внутри liveQuery.
liveQuery(() =>
  db.logs
    .wh ere('level')
    .equals('error')
    .toArray()
)

Типизация реактивных обёрток (TypeScript)

Безопасная работа с состоянием

import { Ref } from 'vue'

interface UseDexieResult<T> {
  state: Ref<T | null>
  error: Ref<Error | null>
}

Типизация composable позволяет фиксировать структуру данных на уровне доменной модели и снижает риск рассинхронизации состояния.


Паттерны архитектуры реактивного слоя

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

Типичная структура приложения с Dexie и Vue включает:

  • слой базы данных (Dexie schema и операции);
  • слой реактивных адаптеров (composable);
  • слой представления (Vue компоненты).

Реактивный слой не содержит бизнес-логики хранения, а только трансформирует поток данных в форму, удобную для UI.


Использование фабрик реактивных запросов

export const createListHook = (table) => {
  return () =>
    useDexieQuery(() => table.toArray())
}

Такой подход снижает дублирование при работе с множеством таблиц с одинаковым паттерном доступа.


Поведение при конкурентных изменениях

Dexie гарантирует консистентность транзакций, однако реактивный слой Vue может получать обновления в быстром темпе. В таких случаях важно:

  • избегать тяжёлых вычислений в подписках;
  • выносить агрегацию в computed;
  • использовать мемоизацию при необходимости.

Обработка ошибок в реактивных потоках

Централизованный контроль состояния ошибки

subscription = liveQuery(queryFn).subscribe({
  next(value) {
    state.value = value
  },
  error(err) {
    error.value = err
  }
})

Ошибки запроса не должны прерывать жизненный цикл компонента, а фиксируются как реактивное состояние.


Поведение при SSR и гидратации

При серверном рендеринге Dexie недоступен, поэтому реактивные обёртки должны учитывать:

  • инициализацию только на клиенте;
  • пустое состояние до монтирования;
  • отсутствие подписок до onMounted.
const isClient = typeof window !== 'undefined'

Реактивные коллекции и масштабирование состояния

При больших объёмах данных предпочтительно:

  • хранить только идентификаторы в состоянии;
  • загружать детали по необходимости;
  • разделять подписки по сегментам данных.

Это снижает нагрузку на liveQuery и Vue reactivity system.