State Management

Server-state против client-state

Дашборд аналитики грузит список проектов из API и кладёт его в Redux-стор, чтобы был под рукой. Через минуту другой пользователь переименовал проект, но на экране всё ещё старое имя, потому что копия в сторе об этом не знает. Команда дописывает ручную инвалидацию: после каждой мутации диспатчит экшен, перетягивает данные, сверяет id. Постепенно половина reducer-ов превращается в самописный кэш с багами - тот самый кэш, который давно решён готовыми библиотеками. Корень в одной подмене: ответ сервера приняли за состояние приложения, хотя это снимок чужих данных с истекающим сроком годности.

  • Linear и Vercel: серверные данные держит кэш-слой (Query/SWR), а Redux или Zustand остаётся только под локальный UI-стейт
  • Любая админка с таблицами и фильтрами: строки таблицы это серверный кэш, а выбранные фильтры до отправки - клиентское состояние
  • E-commerce: каталог, цены и остатки приходят с сервера и устаревают, корзина до оформления живёт на клиенте
  • Команды массово выпиливают сотни строк Redux-слоя загрузки данных, оставляя стор под модалки, тему и черновики форм
  • GitHub по докладам инженеров использует stale-while-revalidate, показывая кэш мгновенно и сверяя его в фоне

Предварительные знания

  • Таксономия состояния: какие вообще бывают виды стейта в приложении
  • Загрузка данных через fetch и обработка loading/error
  • Базовое понимание кэша и срока его жизни (TTL)
  • Таксономия состояния

Как server-state выделили в отдельную категорию

Долгое время стандартом было одно хранилище на всё: ответы API складывали в Redux рядом с флагами модалок и темой. Таннер Линсли в 2020 году с выходом React Query сформулировал то, что многие чувствовали, но не называли: серверные данные принципиально отличаются от состояния приложения. Они принадлежат не клиенту, а серверу. Они устаревают в любой момент. Их разделяют несколько компонентов. Их нужно периодически сверять с источником. Значит, это кэш со сроком годности, а не переменная. Это разделение к 2026 году стало базовым принципом во всех фреймворках - React, Vue, Svelte, Solid - и определило целый класс инструментов: Query, SWR, Apollo, urql.

Два вида состояния

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

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

  • Клиентское состояние — Принадлежит интерфейсу, всегда актуально, синхронное, владелец один. Примеры: открытая модалка, активная вкладка, черновик формы, тема. Инструмент - useState, Zustand, Redux.
  • Серверное состояние — Принадлежит серверу, может устареть, асинхронное, разделяемое многими. Примеры: список заказов, профиль, цены, остатки. Инструмент - TanStack Query, SWR, Apollo.
СвойствоКлиентскоеСерверное
ВладелецИнтерфейсСервер
АктуальностьВсегда истинаСнимок, устаревает
ДоступСинхронныйАсинхронный
Кто меняетТолько этот клиентЛюбой клиент или процесс
Что нужноПрисваиваниеСвежесть, сверка, инвалидация

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

Что из перечисленного - серверное состояние, а не клиентское?

Серверные данные это кэш с TTL

Раз серверная копия устаревает, относиться к ней нужно как к кэшу, а не как к собственному состоянию. У кэша есть срок свежести (TTL): сколько данные считаются актуальными, прежде чем их стоит пересверить с источником. Пока срок не истёк, кэш отдают мгновенно и в сеть не идут. После истечения данные помечаются устаревшими и при следующем обращении тихо перезапрашиваются в фоне. Это и есть модель stale-while-revalidate: показать кэш сразу, сверить с сервером следом.

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

  • TTL (staleTime): срок, в течение которого снимок считается свежим и не перезапрашивается
  • Background revalidation: после истечения срока кэш тихо сверяется с сервером в фоне
  • Дедупликация: десять компонентов с одним ключом дают один сетевой запрос, а не десять
  • Единый источник: все читатели ключа видят одни и те же данные, копий не возникает

Срок свежести подбирают под природу данных. Курс валют или лента новостей - секунды. Профиль пользователя или справочник стран - минуты или часы. Чем реже данные меняются на сервере, тем дольше снимок можно считать свежим.

Почему серверные данные правильнее вести как кэш с TTL, а не как переменную в сторе?

Антипаттерн номер один

Самая частая ошибка в работе с серверными данными: скопировать ответ API в глобальный стор (Redux или Zustand) и при этом параллельно пользоваться fetch-кэшем. Возникают две копии одних и тех же данных, и они расходятся. Кэш сходил в сеть и обновился, а копия в сторе осталась старой. Или наоборот - вручную обновили стор, а кэш отдаёт прежний снимок. Дальше команда дописывает ручную синхронизацию, и reducer-ы превращаются в самописный кэш-слой с багами.

Признаки, что в проект заехал этот антипаттерн: после каждой мутации диспатчится экшен ради синхронизации; в сторе лежат массивы с id записей, пришедших из API; reducer-ы наполнены логикой 'обнови элемент по id'. Всё это - ручная реализация кэша, который уже умеют делать готовые библиотеки.

Разделение простое и держится одним правилом. Серверное состояние ведёт кэш-библиотека: Query, SWR, Apollo или urql - она владеет снимком, его свежестью и сверкой. Глобальный стор (Redux, Zustand) остаётся под клиентское состояние: модалки, тема, выбранные фильтры до отправки, черновики. Две роли не пересекаются, и копий одних данных не возникает.

Команда грузит список из API в useEffect и кладёт его в Redux, а рядом тот же запрос идёт через кэш-библиотеку. В чём здесь главная проблема?

Связь с другими темами

Опорный урок блока про серверное состояние. Дальше тема раскрывается инструментами:

  • TanStack Query — Главный инструмент, который ведёт серверный кэш со свежестью, фоновой сверкой и инвалидацией
  • SWR — Лёгкая альтернатива от Vercel с той же моделью stale-while-revalidate
  • GraphQL-кэш как состояние — В Apollo и urql нормализованный кэш сам и есть серверное состояние

Итог

  • Состояние делится на два вида: клиентское принадлежит интерфейсу и всегда актуально, серверное это снимок данных с сервера, который устаревает
  • Серверные данные - удалённый кэш с TTL, а не состояние, которым владеет клиент. У него есть срок свежести и стратегия обновления, а не простое присваивание
  • Антипаттерн номер один: копировать ответ API в глобальный стор (Redux/Zustand) и параллельно держать fetch-кэш - получаются две копии, расходящиеся между собой
  • Признак проблемы: ручная инвалидация после каждой мутации, диспатч экшенов ради синхронизации, reducer-ы, превратившиеся в самописный кэш с багами
  • Правильный путь: серверное состояние ведёт кэш-библиотека (Query/SWR/Apollo), а глобальный стор остаётся только под чисто клиентский стейт

Связанные уроки

  • sm-02-state-taxonomy — Деление состояния на виды из таксономии - фундамент, на котором стоит разделение server-state и client-state
  • sm-31-tanstack-query — Поняв, что серверные данные это кэш, переходят к инструменту, который этот кэш ведёт - TanStack Query
  • rc-36-tanstack-query — Тот же разрыв server-state и client-state, разобранный в контексте React-экосистемы
Server-state против client-state

0

1

Войти