Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Архитектура состояния и данных
state_data_architecture

Архитектура состояния и данных

Данные сервера, клиентское состояние, владелец кэша, оптимистичные изменения, состояние URL и выбор хранилища.

Открыть лабораториюv0.2.0Запускается локально из публичного репозитория

Архитектура состояния и данных

Большинство проблем управления состоянием начинаются, когда данные разных владельцев и с разным сроком жизни складывают в одно хранилище.

#Результат урока

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

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

#1. Классификация по владельцу и сроку жизни

Различайте локальное состояние интерфейса, черновик формы, URL, серверные данные и кеш, сеанс и внешние данные реального времени. Для каждого укажите главный источник (source of truth), область действия, сохранение, источник обновления и требования согласованности. Одно «глобальное состояние» скрывает различия и создаёт неверные правила сброса и обновления.

Карту владельцев полезно писать до выбора библиотеки — тогда видно, что данные из одной таблицы требуют разных механизмов:

ДанныеВладелецСрок жизниМеханизм
Наведение, раскрытая строкакомпонентдо размонтированияuseState
Черновик формыкомпонент формыдо отправкиuseState / useReducer
Фильтр, сортировка, страницаURLдо смены ссылкипараметры поиска
Список заявок, справочникисервердо истечения свежестикеш запросов
Профиль и права сеансасервердо выходапровайдер + серверная проверка

Строки этой таблицы нельзя объединить: у наведения нет удалённого владельца, у списка заявок нет смысла в сохранении между сеансами, а токен доступа не должен попадать в сохраняемое хранилище вообще.

Проверьте себя. Заполните таблицу данных продукта до выбора библиотеки.

Частая ошибка. Положить наведение, токен доступа и серверный список в одно сохраняемое хранилище.

#2. Серверные данные не равны клиентскому состоянию

У серверных данных есть удалённый владелец; они могут устареть, загружаться с ошибкой и меняться без клиента. Кеш хранит снимок с ключом, сроком свежести и правилом обновления. Копирование результата запроса в клиентское хранилище создаёт второй источник истины, если нет независимого черновика или сценария.

Второй источник истины обычно появляется как «безобидный» эффект синхронизации:

// ❌ src/features/tickets/useTickets.ts — копия серверных данных в хранилище const { data } = useQuery({ queryKey: ['tickets'], queryFn: fetchTickets }); useEffect(() => { if (data) dispatch(setTickets(data)); // теперь актуальных списков два }, [data, dispatch]);

После такого эффекта любое обновление кеша нужно вручную дублировать в хранилище, а любая ошибка синхронизации даёт интерфейс, показывающий устаревшие данные. Правильный вариант — читать данные там, где они уже есть:

// ✅ один владелец: кеш запросов const { data: tickets = [], isFetching } = useQuery({ queryKey: ['tickets'], queryFn: fetchTickets, });

Клиентское хранилище остаётся нужным только для того, чего нет на сервере: черновиков, выбранных элементов, режимов интерфейса.

Проверьте себя. Удалите эффект, копирующий результат запроса в Redux или локальное состояние.

Частая ошибка. После каждого fetch отправлять полный список ещё в одно хранилище.

#3. Детерминированные ключи кеша

Ключ включает все входы, влияющие на результат: ресурс, идентификатор, фильтры, организацию и пользователя. Один логический запрос получает один ключ; разные области авторизации не делят приватные данные. Объекты параметров приводят к канонической форме по правилам библиотеки.

Ключи стоит собирать в одном месте — тогда их можно протестировать и нельзя случайно «забыть» вход:

// src/features/tickets/keys.ts export const ticketKeys = { all: (orgId: string) => ['tickets', orgId] as const, list: (orgId: string, filters: TicketFilters) => [...ticketKeys.all(orgId), 'list', filters.status, filters.page] as const, detail: (orgId: string, id: number) => [...ticketKeys.all(orgId), 'detail', id] as const, };

Ключ ['tickets'] без orgId — это не только неверный кеш, но и утечка между арендаторами: пользователь второй организации получит из кеша данные первой. Явное перечисление полей фильтра вместо целого объекта защищает от того, что новое поле фильтра появится, а ключ останется прежним.

Проверьте себя. Создайте функцию ключей и тесты для фильтров и организаций.

Частая ошибка. Один ключ products для всех организаций и фильтров.

#4. Свежесть и обновление кеша

Срок свежести (staleTime) — продуктовое правило допустимой давности, а не магическое ускорение. При обновлении связанные записи помечаются устаревшими или заменяются данными ответа. Общее обновление проще, но дороже; точечное требует знать зависимости. Время сборки мусора регулирует память, а не свежесть.

Разные данные заслуживают разных правил, и это решение предметное, а не техническое:

// src/features/catalog/queries.ts const currencies = useQuery({ queryKey: ['currencies'], queryFn: fetchCurrencies, staleTime: 24 * 60 * 60 * 1000, // справочник меняется раз в сутки }); const stock = useQuery({ queryKey: ['stock', sku], queryFn: () => fetchStock(sku), staleTime: 15 * 1000, // остаток склада устаревает почти сразу });

Если поставить справочнику staleTime: 0, приложение будет тратить сеть на данные, которые не меняются. Если поставить остатку сутки — пользователь продаст товар, которого нет. gcTime в обоих случаях отвечает только за то, когда неиспользуемый снимок выгрузят из памяти.

Проверьте себя. Определите срок свежести отдельно для справочника и остатков склада.

Частая ошибка. Считать динамические данные вечно свежими без внешних уведомлений или обновления кеша.

#5. Предварительное обновление как транзакция

Перед изменением снимите затрагиваемый кеш и примените предварительную правку (optimistic update). При отказе откатите её или перечитайте данные, при успехе согласуйте ответ сервера. Параллельным изменениям нужны идентификатор и версия. Слепой откат старого снимка может стереть более новое успешное изменение.

Транзакция состоит из четырёх шагов, и каждый из них обязателен:

// src/features/cart/useUpdateQuantity.ts useMutation({ mutationFn: updateQuantity, onMutate: async (variables) => { await queryClient.cancelQueries({ queryKey: ['cart'] }); // 1. остановить конкурентов const snapshot = queryClient.getQueryData<Cart>(['cart']); // 2. снять снимок queryClient.setQueryData<Cart>(['cart'], (cart) => applyQuantity(cart, variables)); return { snapshot }; }, onError: (_error, _variables, context) => { // 3. откат именно того снимка, который сняла эта мутация if (context?.snapshot) queryClient.setQueryData(['cart'], context.snapshot); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ['cart'] }); // 4. согласовать с сервером }, });

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

Проверьте себя. Завершите два изменения количества в обратном порядке.

Частая ошибка. Одно общее предыдущее состояние для всех параллельных изменений.

#6. Конфликты версий и сохранение намерения

При конфликте редактирования сервер возвращает несовпадение версии или ETag. Интерфейс не должен молча перезаписывать чужое изменение или терять черновик. Покажите актуальное серверное значение, локальный черновик и варианты объединения или повтора. Правило «последняя запись побеждает» (last write wins) допустимо только как осознанное решение предметной области.

Версия передаётся на сервер и проверяется им; клиент лишь корректно обрабатывает отказ:

// src/features/tickets/save.ts const response = await fetch(`/api/tickets/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'If-Match': etag }, body: JSON.stringify(draft), }); if (response.status === 409) { const server = (await response.json()) as Ticket; // черновик не выбрасываем: пользователь решает, что оставить return { kind: 'conflict', server, draft } as const; }

Автоматический повтор того же PUT после 409 — это и есть молчаливая перезапись чужой работы: сервер отказал именно потому, что данные изменились. Экран сравнения стоит дороже в разработке, но сохраняет и намерение пользователя, и чужое изменение.

Проверьте себя. Смоделируйте 409 и экран сравнения.

Частая ошибка. Автоматически повторить PUT без версии после ответа 409.

#7. Сохранение и миграция схемы

Сохранённое клиентское состояние становится долговременным форматом. Храните минимум, добавляйте версию и правила миграции или очистки, не сохраняйте секреты и серверный кеш без необходимости. localStorage синхронен, доступен скриптам и может содержать повреждённую строку — чтение начинается с unknown и try/catch.

Чтение сохранённых настроек — это разбор недоверенного ввода, а не восстановление объекта:

// src/shared/settings/storage.ts const CURRENT_VERSION = 2; export function readSettings(): Settings { try { const raw: unknown = JSON.parse(localStorage.getItem('settings') ?? 'null'); if (!isRecord(raw)) return defaultSettings; if (raw.version === 1) return migrateV1toV2(raw); // старый формат ещё в браузерах if (raw.version === CURRENT_VERSION) return parseSettings(raw); return defaultSettings; // формат из будущей версии читать не пытаемся } catch { return defaultSettings; // повреждённая строка не должна ломать приложение } }

Обратите внимание на три отдельных исхода: старая версия — миграция, текущая — разбор, всё остальное — значения по умолчанию. Без версии в записи вы не отличите одно от другого, и первое же изменение формы настроек уронит приложение у существующих пользователей.

Проверьте себя. Перенесите настройки с версии 1 на версию 2 и обработайте неверный JSON.

Частая ошибка. Сохранять всё глобальное хранилище, включая токен доступа и временные ошибки.

#8. Выбор библиотеки через требования

Сравнивайте решения по селекторам, средствам диагностики, асинхронной работе, кешу, SSR, размеру пакета, экосистеме и опыту команды. Redux Toolkit подходит сложным клиентским переходам, TanStack Query — серверному кешу, Context — передаче стабильной зависимости, загрузчики маршрутов — данным навигации. Комбинация нормальна при чётких границах.

Обоснование выбора звучит как связка «требование → механизм», и в нём обязательно есть отказы:

Фильтры и страница → URL: нужен обмен ссылкой и кнопка «назад». Список и деталь заявки → TanStack Query: удалённый владелец, свежесть 30 с, предварительные правки. Черновик формы → useReducer в форме: живёт до отправки, инварианты проверяются переходами. Сеанс → Context над результатом серверной проверки: стабильная зависимость без частых обновлений. Redux Toolkit → не берём: нет сложных клиентских переходов, которые не покрываются перечисленным.

Последняя строка — самая полезная. Выбор одного хранилища «чтобы всё было в одном месте» обычно означает, что серверный кеш придётся написать вручную, а вместе с ним свежесть, дедупликацию запросов и откат правок.

Проверьте себя. Кратко обоснуйте набор средств состояния, включая отказ от ненужных слоёв.

Частая ошибка. Выбрать одно хранилище для всего только ради единого инструмента диагностики.

#Лаборатория

Спроектируйте интерфейс склада: фильтры в URL, черновик в состоянии формы, сеанс в провайдере, товары в серверном кеше. Добавьте срок свежести, функцию ключей, предварительное изменение количества, конфликт версии 409 и правило отката или повторной загрузки.

Работа готова, когда серверные сущности без причины не копируются в глобальное клиентское хранилище, ключи кеша детерминированы, а конфликт не теряет ввод пользователя.

#Источники для сверки

  • React: Managing State
  • TanStack Query overview
  • Redux style guide

Устранение неисправностей

Далее: Тестирование React-приложений