Вы сделали страницу со списком заметок. Сначала приложение получает данные с сервера и показывает их пользователю. Затем появляется кнопка обновления, несколько компонентов начинают обращаться к одному API, а после сохранения заметки список должен измениться.
Можно самостоятельно управлять запросами через useEffect и useState. Можно использовать React Query, который берёт на себя часть работы с серверными данными. Но в Next.js App Router есть и другой вариант. Получать данные непосредственно в серверных компонентах. Разберём, чем отличаются эти подходы и когда стоит использовать каждый из них.
Почему серверные данные не похожи на обычное состояние
Представим компонент с кнопкой раскрытия меню. Значение isOpen принадлежит интерфейсу. Его можно хранить в useState, изменять после нажатия и не синхронизировать с внешним сервисом.
Список заметок устроен иначе. Данные находятся на сервере, а браузер получает лишь их текущую версию. Пока пользователь читает страницу, запись может измениться в другой вкладке или на другом устройстве.
Поэтому приложению приходится решать несколько вопросов. Когда отправлять запрос, как показывать загрузку, что делать при ошибке, можно ли повторно использовать предыдущий результат и когда получать свежие данные. Это задачи управления серверным состоянием. Их можно решать самостоятельно, но по мере развития приложения логика начинает повторяться.
Что делает React Query
React Query, который сейчас развивается как TanStack Query, помогает загружать, кэшировать и обновлять данные из асинхронных источников. Вместо того чтобы отдельно создавать состояния для результата, загрузки и ошибки, компонент может использовать хук useQuery. Для запроса ему нужны два основных параметра:
queryKey, идентификатор набора данных. По нему библиотека различает запросы и связывает их с кэшем.
queryFn, функция, которая выполняет загрузку и возвращает Promise. Если запрос не удался, функция должна сообщить об ошибке.
React Query предоставляет состояния запроса и позволяет обновлять данные, не разрабатывая всю эту механику заново.
Подключаем React Query
Для примера возьмём Next.js App Router. Установим библиотеку:
npm install @tanstack/react-query
Затем создадим клиентский компонент, который предоставляет экземпляр QueryClient другим компонентам приложения.
// app/providers.tsx
"use client";
import { useState } from "react";
import {
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
export default function Providers({
children,
}: {
children: React.ReactNode;
}) {
const [client] = useState(() => new QueryClient());
return (
<QueryClientProvider client={client}>
{children}
</QueryClientProvider>
);
}
Этот компонент нужно подключить в корневом app/layout.tsx, обернув в него содержимое приложения. Сам layout.tsx при этом может оставаться серверным компонентом. QueryClient управляет кэшем запросов. Мы создаём его один раз для данного экземпляра провайдера, чтобы он не пересоздавался при каждом обновлении интерфейса.
В более сложных приложениях, использующих предварительную серверную загрузку и гидратацию, настройка клиента требует дополнительных правил.
Загружаем список заметок через useQuery
Предположим, у приложения уже есть endpoint GET /api/notes, возвращающий JSON-массив заметок. Он может быть реализован через Next.js Route Handler или отдельный backend.
Создадим клиентский компонент, который обращается к этому API.
"use client";
import { useQuery } from "@tanstack/react-query";
type Note = {
id: number;
title: string;
};
async function getNotes(): Promise<Note[]> {
const response = await fetch("/api/notes");
if (!response.ok) {
throw new Error("Не удалось получить заметки");
}
return response.json();
}
export default function NotesList() {
const {
data,
isPending,
isError,
isFetching,
refetch,
} = useQuery({
queryKey: ["notes"],
queryFn: getNotes,
staleTime: 30_000,
});
if (isPending) {
return <p>Загружаем заметки...</p>;
}
if (isError) {
return <p>Не удалось загрузить список</p>;
}
return (
<section>
<button
type="button"
onClick={() => void refetch()}
disabled={isFetching}
>
{isFetching ? "Обновляем..." : "Обновить"}
</button>
<ul>
{data.map((note) => (
<li key={note.id}>{note.title}</li>
))}
</ul>
</section>
);
}
Здесь предполагается, что API уже существует и возвращает данные нужной структуры. Сам по себе useQuery не создаёт серверный маршрут и не проверяет содержимое JSON.
При первом обращении компонент показывает состояние загрузки. После успешного ответа отображает заметки. Если запрос завершается ошибкой, показывает соответствующее сообщение. Кнопка вызывает refetch(), позволяя пользователю обновить список. При этом библиотека может сохранять предыдущие данные, пока выполняет повторную загрузку.
Зачем нужен queryKey
Ключ ["notes"] обозначает конкретный запрос. Если другой компонент использует такой же ключ и получает данные из того же источника, он может обращаться к общему кэшу. Когда у списка появляются фильтры, ключ должен учитывать параметры, влияющие на результат, например:
useQuery({
queryKey: ["notes", category],
queryFn: () => getNotesByCategory(category),
});
В этом примере getNotesByCategory обозначает существующую функцию загрузки. При изменении category React Query будет работать с другим ключом и соответствующим набором данных.
Если забыть включить параметр в queryKey, библиотека может считать разные варианты поиска одним и тем же запросом. Поэтому ключ описывает не название компонента, а данные, которые он получает.
Что означает staleTime
В примере мы указали staleTime: 30_000. Это означает, что успешно загруженные данные считаются свежими в течение 30 секунд. Пока данные свежие, React Query может использовать кэш без некоторых автоматических повторных запросов. После истечения этого периода данные считаются устаревшими, но не обязательно немедленно удаляются или загружаются заново. Свежесть данных и их существование в кэше разные характеристики. Повторная загрузка зависит также от настроек библиотеки, подключения компонента, возвращения к вкладке и других событий.
Если заметки изменяются редко, период свежести может быть больше. Если несколько пользователей одновременно редактируют общий список, требования к обновлению будут другими.
Для работы с изменениями данных после создания или удаления записи используют мутации и обновление либо инвалидирование связанных запросов. Одна лишь настройка staleTime не решает эту задачу.
Почему не всегда нужен React Query
В Next.js App Router серверный компонент может получить данные до того, как сформирует страницу. Предположим, список заметок нужен только для первоначального отображения. Если данные доступны серверу, можно загрузить их непосредственно в серверном компоненте.
// app/notes/page.tsx
export default async function NotesPage() {
const response = await fetch(
`${process.env.API_BASE_URL}/notes`
);
if (!response.ok) {
throw new Error("Не удалось загрузить заметки");
}
const notes: { id: number; title: string }[] =
await response.json();
return (
<ul>
{notes.map((note) => (
<li key={note.id}>{note.title}</li>
))}
</ul>
);
}
API_BASE_URL серверная переменная окружения с адресом существующего API. Такой пример не требует React Query, потому что данные загружаются на сервере для формирования страницы. Но у серверной загрузки есть собственные правила кэширования, повторного получения и обновления интерфейса. Если данные должны меняться прямо во время работы пользователя, понадобятся дополнительные механизмы.
В моей статье на Habr о кэшировании Next.js App Router разобрано, почему данные на странице иногда не обновляются тогда, когда разработчик ожидает.
Как выбрать подход для своего приложения
Для страницы статьи, карточки товара или первоначального каталога часто достаточно серверной загрузки Next.js. Она позволяет получить данные в подходящей серверной среде и подготовить содержимое страницы.
React Query имеет смысл рассмотреть, когда пользователь долго работает с интерактивным экраном, переключает фильтры, обновляет списки и изменяет записи без полной навигации. В таких сценариях пригодятся клиентский кэш, управление свежестью данных и повторные запросы.
Эти подходы можно сочетать. Например, Next.js может предварительно получить данные на сервере, а React Query принять их в клиентский кэш через механизм гидратации и продолжить обновление в браузере. Но такая схема сложнее первого примера и не нужна каждой странице. Подробности настройки есть в документации TanStack Query для Server Components.
От запросов к архитектуре приложения
В демонстрационном Workbench Notes можно посмотреть приложение на Next.js с заметками, формами и серверными изменениями данных.
Курс «Next.js II: TypeScript 2026» разбирает работу с данными через типизированные контракты, Server Actions, обновление кэша и состояние интерфейса. React Query рассмотрен как дополнительный подход для клиентских запросов, а не как библиотека из программы курса.
Если данные нужны для первоначальной серверной страницы, начните с возможностей Next.js. Если интерфейсу требуется собственный цикл загрузки, обновления и переиспользования данных в браузере, рассмотрите React Query. Выбор зависит от поведения приложения, а не от количества библиотек в проекте.