Большинство фронтенд-разработчиков ненавидят две вещи:
ложные повторные запросы в данных и уверенное поведение по кэшированию. Обычное решение — ручное управление состоянием через Redux или Context, перегруженное useEffect
, и неисчезающая вероятность возникновения гонки данных. Тут начинается свет в конце тоннеля: React Query не просто библиотека для запросов. Это система управления асинхронным состоянием, которая пересматривает наши приемы работы с серверными данными.
Почему ручное управление проваливается
Предположим, вы загружаете список пользователей:
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
fetch('/api/users')
.then(res => res.json())
.then(data => setUsers(data))
.finally(() => setIsLoading(false));
}, []);
Уже здесь проблемы:
- Кэширование отсутствует — при переходе между маршрутами данные запрашиваются повторно
- Фоновое обновление не происходит — вкладка открыта час, а данные устарели
- Кол-во состояния управления —
isLoading
,error
,isRefetching
нужно добавлять вручную - Критическая ошибка: данные сбрасываются при изменении стейта компонента (например, при переключении вкладки внутри компонента)
React Query заменяет данный шаблон с помощью декларативного API.
Основы: Запросы как изначальные состояния
Установите и подключите QueryClientProvider к приложению. Затем:
import { useQuery } from '@tanstack/react-query';
const fetchUsers = async () => {
const res = await fetch('/api/users');
if (!res.ok) throw new Error('Network response error');
return res.json();
};
function UsersList() {
const {
data: users,
isLoading,
isError,
error
} = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
// Автоматические возвраты отображения через состояние
if (isLoading) return <Spinner />;
if (isError) return <Error message={error.message} />;
return users.map(user => <UserCard key={user.id} {...user} />);
}
Кажется знакомо? Но под капотом произошло следующее:
- Результат запроса кэшируется под ключом
['users']
- При повторном использовании используется кэш, в фоне делается повторный запрос (stale-while-revalidate)
- Данные остаются в кеше и после анмаунта компонента (настраиваемое время хранения с помощью
gcTime
)
Инвалидация: Принудительное обновление без боли
Добавить нового пользователя? Обновить список после мутации:
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: addNewUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
// Только компоненты, подписанные на ['users'], перерендерятся
}
});
invalidateQueries
помечает запрос как невалидный. Все активные компоненты выполнят фоновый перезапрос. Никаких ручных setState
, никаких лишних сетевых вызовов на компонентах, которые скрыты.
Префетчинг: Предотавка данных для мгновенного UX
Когда пользователь наводится на ссылку профиля, загрузка данных начинается заранее:
const queryClient = useQueryClient();
const prefetchUser = (userId) => {
queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000 // Храним как "свежие" 5 минут
});
};
// Кнопки-карточки юзеров со слоем hover
const UserLink = ({ id, name }) => (
<Link
to={`/user/${id}`}
onMouseEnter={() => prefetchUser(id)}
>
{name}
</Link>
);
Это устраняет индикаторы загрузки при переходе — данные уже в кеше.
Типизация с TypeScript: Предиктивные строки для ключей
React Query интегрирован с TypeScript через дженерики:
type User = { id: number; name: string };
type UserError = Error;
const { data } = useQuery<User[], UserError>({
queryKey: ['users', { activeOnly: true }] // Ключ детализируется автоматически
queryFn: fetchUsers
});
Ключи запросов сериализуются детерминированно: ['users', { activeOnly: true }]
и ['users', { activeOnly: true }]
считаются идентичными. Нелепые JSON.stringify
не требуются.
Оптимизации, которые работают как по волшебству
- Дублирующие запросы: Запросы с одинаковым ключом внутри одного интервала
staleTime
объединяются в один сетевой вызов. - Переиспользование кеша: При одинаковых ключах разных компонентов происходит синхронное обновление (
observer
радиального состояния). - Умный перезапрос: Фоновый запрос происходит только когда компонент видим или при позиционировании окна (
refetchOnWindowFocus
по умолчаниюtrue
).
Когда не использовать React Query?
Если данные синхронны (например, геолокация из браузера) или состояние полностью локально (draggable UI), задействовать хук состояния useState
вполне достаточно. Также для сложных сетевых особенностей (WebSockets) может потребоваться сочетание с useWebSocket
.
Вывод: Меньше копоти, больше свежих данных
React Query — это ракетное топливо для асинхронного состояния. Новым разработчикам это устраняет больуправленческого состояния. Опытные команды начинают перестраивать архитектуру приложений вокруг кешированного запроса вместо глобальных хранилищ. Тратить подумаек состояния на формулы бессмысленно.
Применение элементарно:
- Замените все
useEffect
запросы наuseQuery
- Мутации централизуйте через
useMutation
- Требуйте от бэкенда различимые ключи (
user:id
) - Настройте префетчинг для ключевых пользовательских путей
Не верьте на слово — замените свои буксующие fetch
в одном компоненте. Динамическое кэширование демпфирует бэкендный поток, сокращает цепи рендеринга и возвращает фронтенду его правдивое сердце — UI.