Модель данных и нормализация состояния

↑ 8.4 Frontend System Design · ← Предыдущая · → Следующая

Желательно~3 мин чтенияУровень: senior

Зачем это на собесе

Хранение данных на клиенте определяет консистентность и производительность UI.

Категории состояния

ВидГдеИнструмент
Серверные данные (кэш)клиентский кэшTanStack Query, Apollo
Глобальное UI-состояниесторPinia
Локальное состояниекомпонентref
Состояние URLадресная строкаVue Router
Формалокальноvee-validate, FormKit
ПостоянноеlocalStorage, IndexedDB—

Проблема вложенных данных

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

Нормализация

Храним сущности в плоских таблицах по id, связи — ссылками.

// вложенный ответ
{ posts: [{ id: 1, author: { id: 7, name: 'Аня' }, comments: [{ id: 3, author: { id: 7, name: 'Аня' } }] }] }
 
// нормализованное состояние
{
  users: { 7: { id: 7, name: 'Аня' } },
  posts: { 1: { id: 1, authorId: 7, commentIds: [3] } },
  comments: { 3: { id: 3, authorId: 7 } },
  postIds: [1],
}

Плюсы: единое место правки, дешёвые обновления, нет дублей и рассинхронизации. Минусы: нужен код сборки (denormalize) и нормализации; библиотеки: normalizr, нормализованный кэш Apollo, Pinia ORM.

Оптимистичные обновления

const mutation = useMutation({
  mutationFn: likePost,
  onMutate: async (id) => {
    await qc.cancelQueries({ queryKey: ['post', id] })
    const prev = qc.getQueryData(['post', id])
    qc.setQueryData(['post', id], (p: Post) => ({ ...p, liked: true, likes: p.likes + 1 }))
    return { prev }
  },
  onError: (_e, id, ctx) => qc.setQueryData(['post', id], ctx!.prev),
  onSettled: (_d, _e, id) => qc.invalidateQueries({ queryKey: ['post', id] }),
})

Консистентность

  • инвалидация связанных запросов после мутации;
  • версии, ETag и обработка конфликтов;
  • порядок событий при real-time (последнее победит или слияние по версии);
  • временные id для новых объектов до ответа сервера.

Вопросы с ответами