Серверное состояние и TanStack Query
Почему данные с сервера — это не useState, query keys, staleTime и инвалидация.
Обновлено
Что такое серверное состояние
Серверное состояние (server state) = данные, которые живут на сервере (в базе, в API), а в браузере у вас лишь копия: пользователи, товары, заказы, комментарии. Противоположность — клиентское состояние: данные, которые есть только в интерфейсе, — открытая модалка, текст в поле, тема.
Разница не академическая — у них совершенно разные проблемы:
| Клиентское состояние | Серверное состояние | |
|---|---|---|
| Примеры | тема, открытая модалка, шаг мастера | пользователи, товары, заказы |
| Владелец | вы (интерфейс) | сервер — у вас копия |
| Синхронное | да | нет — приходит по сети |
| Может устареть | нет | в любой момент (другой пользователь изменил) |
| Проблемы | где хранить | загрузка, ошибки, кэш, дедупликация, повторные запросы, инвалидация, пагинация |
| Инструменты | useState, URL, Zustand |
Server Components, TanStack Query, SWR |
Классическая ошибка — серверные данные в useState + useEffect. В итоге вы заново (и плохо) реализуете кэш, загрузку, повторы, дедупликацию.
Вариант 1: Server Components (Next.js)
Данные получают на сервере и сразу рендерят HTML. Нет состояния загрузки на клиенте, нет кэша, который нужно синхронизировать в браузере.
export default async function Page() {
const posts = await getPosts() // кэшируется через 'use cache' + cacheTag('posts')
return <PostList posts={posts} />
}Мутация → Server Action → updateTag('posts') → страница перерисовывается со свежими данными.
Вариант 2: TanStack Query (на клиенте)
Библиотека, которая управляет серверным состоянием в браузере: кэш, повторные запросы, дедупликация, повторы при ошибке, пагинация, мутации.
const { data, isPending, error } = useQuery({
queryKey: ['posts', { page }], // идентичность данных в кэше
queryFn: () => api(`/api/posts?page=${page}`),
staleTime: 60_000, // свежие 1 мин → без повторного запроса
})
const queryClient = useQueryClient()
const createPost = useMutation({
mutationFn: (post: NewPost) => api('/api/posts', { method: 'POST', body: JSON.stringify(post) }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
})Ключевые понятия
| Понятие | Что означает |
|---|---|
| queryKey | массив, идентифицирующий данные; всё, что влияет на результат, входит в ключ (page, фильтры) |
| staleTime | сколько данные считаются «свежими»; по умолчанию 0 = всегда устаревшие |
| gcTime | сколько данные остаются в кэше, когда их никто не использует (по умолчанию 5 мин) |
| invalidateQueries | помечает устаревшими → повторный запрос для того, что на экране; совпадение по префиксу |
| дедупликация | 3 компонента запрашивают ['user', 1] → один запрос |
| автоматический повторный запрос | при фокусе окна, при переподключении, при монтировании (если устарели) |
Оптимистичное обновление
Показываете результат до ответа сервера, чтобы интерфейс казался мгновенным; при ошибке откатываете.
| Где | Как |
|---|---|
| React 19 / Server Actions | useOptimistic |
| TanStack Query | onMutate (обновляете кэш) + onError (откат) |
Как выбрать
| Ситуация | Выбор |
|---|---|
| страницы Next, которые показывают данные (блог, каталог, профиль) | Server Components |
| мутации из форм в Next | Server Actions + updateTag |
| очень интерактивный интерфейс с серверными данными: дашборд, бесконечная прокрутка, polling, живые фильтры | TanStack Query (можно делать prefetch на сервере) |
| SPA без собственного сервера | TanStack Query |
| чисто интерфейсное состояние | useState / URL / Zustand |
Коротко
- Серверное состояние = локальная копия данных с сервера; может устареть в любой момент.
- Не кладите его в
useState+useEffect. - Next: Server Components + Server Actions; на клиенте: TanStack Query с
queryKey,staleTime, инвалидацией.